Введение
В этой серии постов мы расскажем о портировании игры Detroit: Become Human с PlayStation 4 на PC.
Detroit: Become Human была выпущена на PlayStation 4 в мае 2018 года. Мы начали работу над версией для PC в июле 2018 года, а выпустили её в декабре 2019 года. Это адвенчура с тремя играбельными персонажами и множеством сюжетных линий. Она имеет очень качественную графику, а большинство графических технологий было разработано самой компанией Quantic Dream.
3D-движок обладает отличными возможностями:
- Реалистичный рендеринг персонажей.
- PBR-освещение.
- Высококачественная постобработка, например Depth of Field (DOF), motion blur, и так далее.
- Временно́е сглаживание.
Detroit: Become Human
С самого начала 3D-движок игры проектировался специально для PlayStation, и мы понятия не имели, что позже он будет поддерживать другие платформы. Поэтому версия для PC стала для нас сложной задачей.
- Руководитель разработкой 3D-движка Ронан Маршалот и ведущие разработчики 3D-движка Николас Визери и Джонатан Сирет из Quantic Dream расскажут об аспектах рендеринга портированной игры. Они объяснят, какие оптимизации можно было без проблем перенести с PlayStation 4 на PC, и с какими сложностями, вызванными различиями между платформами, они столкнулись.
- Лу Крамер — это инженер по разработке технологий из AMD. Она помогала нам оптимизировать игру, поэтому подробно расскажет о неоднородном индексировании ресурсов на PC и, в частности, в картах AMD.
Выбор графического API
У нас уже была OpenGL-версия движка, которую мы применяли в наших инструментах разработки.
Но нам не хотелось выпускать игру на OpenGL:
- У нас было множество проприетарных расширений, которые были открыты не всем производителям GPU.
- Движок имел очень низкую производительность в OpenGL, хотя, разумеется, её можно было оптимизировать.
- В OpenGL существует множество способов реализации разных аспектов, поэтому правильная реализация разных аспектов на всех платформах превращалось в кошмар.
- Инструменты OpenGL не всегда надёжны. Иногда они не работают, потому что мы используем расширение, которого они не знают.
Из-за активного использования несвязанных ресурсов мы не могли портировать игру на DirectX11. В нём недостаточно слотов под ресурсы, и очень сложно было бы достичь достойной производительности, если бы нам пришлось переделывать шейдеры для использования меньшего количества ресурсов.
Мы выбирали между DirectX 12 и Vulkan, имеющими очень схожий набор функций. Vulkan в дальнейшем позволил бы нам обеспечить поддержку Linux и мобильных телефонов, а DirectX 12 обеспечивал поддержку Microsoft Xbox. Мы знали, что в конечном итоге нам нужно будет реализовать поддержку обоих API, но для порта разумнее будет сосредоточиться только на одном API.
Vulkan поддерживает Windows 7 и Windows 8. Так как мы хотели сделать Detroit: Become Human доступной как можно большему количеству игроков, это стало очень сильным аргументом. Однако портирование занял один год, и этот аргумент уже маловажен, ведь Windows 10 теперь используется очень широко!
Концепции разных графических API
OpenGL и старые версии DirectX имеют очень простую модель управления GPU. Эти API просты в понимании и очень хорошо подходят для обучения. Они поручают драйверу выполнять большой объём работы, скрытый от разработчика. Следовательно, в них очень сложно будет оптимизировать полнофункциональный 3D-движок.
С другой стороны, PlayStation 4 API очень легковесный и очень близок к «железу».
Vulkan находится где-то посередине. В нём тоже есть абстракции, потому что он работает на разных GPU, но разработчики имеют больше контроля. Допустим, у нас есть задача реализации управления памятью или кэша шейдера. Так как драйверу остаётся меньше работы, её приходится делать нам! Однако мы разрабатывали проекты на PlayStation, и поэтому нам удобнее, когда мы можем всё контролировать.
Сложности
Центральный процессор PlayStation 4 — это AMD Jaguar с 8 ядрами. Очевидно, что он медленнее, чем новое оборудование PC; однако PlayStation 4 имеет важные преимущества, в частности, очень быстрый доступ к «железу». Мы считаем, что графический API PlayStation 4 гораздо эффективнее, чем все API на PC. Он очень прямолинейный и мало тратит ресурсов впустую. Это означает, что мы можем добиться большого количества вызовов отрисовки на кадр. Мы знали, что высокое количество вызовов отрисовки может стать проблемой на слабых PC.
Ещё одно важное преимущество заключалось в том, что все шейдеры на PlayStation 4 можно было скомпилировать заранее, то есть их загрузка выполнялась практически мгновенно. На PC драйвер должен компилировать шейдеры во время загрузки игры: из-за большого количества поддерживаемых конфигураций GPU и драйверов этот процесс невозможно выполнить заранее.
Во время разработки Detroit: Become Human на PlayStation 4 художники могли создавать уникальные деревья шейдеров для всех материалов. Из-за этого получалось безумное количество вершинных и пиксельных шейдеров, поэтому мы с самого начала работы над портом знали, что это станет огромной проблемой.
Конвейеры шейдеров
Как мы знаем по нашему OpenGL-движку, компиляция шейдеров может занимать много времени на PC. Во время продакшена игры мы сгенерировали кэш шейдеров по модели GPU наших рабочих станций. Генерация полного кэша шейдеров для Detroit: Become Human заняла целую ночь! Все сотрудники получили доступ к этому кэшу шейдеров на утро. Но игра всё равно тормозила, ведь драйверу нужно было преобразовать этот код в нативный ассемблерный код шейдера GPU.
Оказалось, что Vulkan намного лучше справляется с этой проблемой, чем OpenGL.
Во-первых, Vulkan не использует напрямую высокоуровневый язык шейдеров наподобие HLSL, а вместо него применяет промежуточный язык шейдеров под названием SPIR-V. SPIR-V ускоряет компиляцию шейдеров и упрощает их оптимизацию под компилятор шейдеров драйвера. На самом деле, с точки зрения производительности он сравним с системой кэша шейдеров OpenGL.
В Vulkan шейдеры должны быть связаны, образуя
VkPipeline
. Например, VkPipeline
можно создать из вершинного и пиксельного шейдера. Также он содержит информацию о состоянии рендеринга (тесты глубин, стенсил, смешение и т.д.) и форматы render target. Эта информация важна для драйвера, чтобы он мог обеспечить максимально эффективную компиляцию шейдеров.В OpenGL компиляция шейдеров не знает контекста использования шейдеров. Драйверу нужно дождаться вызова отрисовки, чтобы сгенерировать двоичный файл GPU, и именно поэтому первый вызов отрисовки с новым шейдером может долго выполняться в CPU.
В Vulkan конвейер
VkPipeline
предоставляет контекст использования, поэтому у драйвера есть вся информация, необходимая для генерации двоичного файла GPU, и первый вызов отрисовки не требует излишней траты ресурсов. Кроме того, мы можем обновлять VkPipelineCache
при создании VkPipeline
.Изначально мы пытались создавать
VkPipelines
в первый раз, когда он нам понадобится. Из-за этого возникали торможения, похожие на ситуацию с драйверами OpenGL. После чего VkPipelineCache
обновлялся, и торможение пропадало до следующего вызова отрисовки.Потом мы прогнозировали, что сможем создавать
VkPipelines
во время загрузки, но когда VkPipelineCache
был неактуальным, это было так медленно, что стратегию загрузки в фоновом режиме реализовать не удалось.В конечном итоге, мы решили генерировать все
VkPipeline
во время первого запуска игры. Это полностью устранило проблемы с торможением, но теперь мы столкнулись с новой сложностью: генерация VkPipelineCache
занимала очень много времени.Detroit: Become Human содержит примерно 99 500
VkPipeline
! В игре используется прямой рендеринг (forward rendering), поэтому шейдеры материалов содержат весь код освещения. Следовательно, компиляция каждого шейдера может занимать длительное время.У нас появилось несколько идей по оптимизации процесса:
- Мы оптимизировали данные так, чтобы можно было загружать только промежуточные двоичные файлы SPIR-V.
- Мы оптимизировали промежуточные двоичные файлы SPIR-V с помощью оптимизатора SPIR-V.
- Мы сделали так, чтобы все ядра CPU тратили 100% времени на создание
VkPipeline
.
Кроме того, важная оптимизация была предложена Джеффом Болцем из NVIDIA, и в нашем случае она оказалась очень эффективной.
Многие
VkPipeline
очень похожи. Например, некоторые VkPipeline
могут иметь одинаковые вершинные и пиксельные шейдеры, отличающиеся всего несколькими состояниями рендеринга, например, параметрами стенсила. В таком случае драйвер может считать их одним конвейером. Но если мы создаём их одновременно, один из потоков будет просто простаивать, ожидая, пока другой завершит задачу. По своей природе, наш процесс передавал все похожие VkPipeline
одновременно. Чтобы решить эту проблему, мы просто изменили порядок сортировки VkPipeline
. «Клоны» поместили в конец, и их создание в результате стало занимать гораздо меньше времени.Производительность создания
VkPipelines
очень сильно варьируется. В частности, оно сильно зависит от количества доступных аппаратных потоков. На AMD Ryzen Threadripper с 64 аппаратными потоками оно может занимать всего две минуты. Но на слабых PC этот процесс, к сожалению, может проходить больше 20 минут.Последнее было для нас слишком долго. К сожалению, единственным способом ещё сильнее снизить это время было уменьшение количества шейдеров. Нам требовалось бы изменить способ создания материалов, чтобы как можно большее их количество было общим. Для Detroit: Become Human это было невозможно, потому что художникам пришлось бы переделывать все материалы. Мы планируем реализовать в следующей игре правильный инстансинг материалов, но для Detroit: Become Human уже было слишком поздно.
Индексирование дескрипторов
Для оптимизации скорости вызовов отрисовки на PC мы использовали индексирование дескрипторов при помощи расширения
VK_EXT_descriptor_indexing
. Его принцип прост: мы можем создать набор дескрипторов, содержащий все используемые в кадре буферы и текстуры. Затем мы сможем получать доступ к буферам и текстурам через индексы. Основное преимущество этого заключается в том, что ресурсы связываются только один раз за кадр, даже если используются во множестве вызовов отрисовки. Это очень похоже на использование несвязанных ресурсов в OpenGL.Мы создаём массивы ресурсов для всех типов используемых ресурсов:
- Один массив для всех 2D-текстур.
- Один массив для всех 3D-текстур.
- Один массив для всех кубических текстур.
- Один массив для всех буферов материалов.
У нас есть только основной буфер, изменяемый между вызовами отрисовки (он реализован в виде кольцевого буфера), содержащий индекс дескриптора, ссылающийся на нужный буфер материала и нужные матрицы. Каждый буфер материала содержит индексы используемых текстур.
Благодаря этой стратегии мы могли хранить малое количество наборов дескрипторов, общих для всех вызовов отрисовки и содержащих всю информацию, необходимую для отрисовки кадра.
Оптимизация обновлений наборов дескрипторов
Даже при малом количестве наборов дескрипторов их обновление по-прежнему оставалось узким местом. Обновление набора дескрипторов, если он содержит множество ресурсов, может быть очень затратным. Например, в одном кадре Detroit: Become Human может быть больше четырёх тысяч текстур.
Мы реализовали инкрементные обновления наборов дескрипторов, отслеживая ресурсы, которые становятся видимыми и невидимыми в текущем кадре. Кроме того, это ограничивает размер массивов дескрипторов, потому что им достаточно иметь ёмкость для обработки видимых ресурсов в текущий момент времени. Отслеживание видимости тратит мало ресурсов, потому что мы не используем затратный алгоритм вычисления пересечений с
O(n.log(n))
. Вместо этого мы используем два списка, один для текущего кадра, второй для предыдущего. Перемещение оставшихся видимыми ресурсов из одного списка в другой и изучение оставшихся в первом списке ресурсов помогает определять, какие ресурсы попадают в пирамиду видимости и исчезают из неё.Получившиеся при этих вычислениях дельты хранятся в течение четырёх кадров — мы используем тройную буферизацию, а для вычислений векторов движения объектов со скиннингом требуется наличие ещё одного кадра. Набор дескрипторов должен оставаться неизменяемым в течение не менее чем четырёх кадров, прежде чем его снова можно будет модифицировать, потому что он по-прежнему может пригодиться GPU. Поэтому мы применяем дельты к группам из четырёх кадров.
В конечном итоге, эта оптимизация снизила время обновления наборов дескрипторов на один-два порядка величин.
Батчинг примитивов
Использование индексирования дескрипторов позволяет нам выполнять батчинг множества примитивов в одном вызове отрисовки с помощью
vkCmdDrawIndexedIndirect
. Мы используем gl_InstanceID
для доступа к нужным индексам в основном буфере. Примитивы можно сгруппировать в батчи, если они имеют одинаковый набор дескрипторов, одинаковый конвейер шейдеров и одинаковый буфер вершин. Это очень эффективно, особенно во время проходов глубины и теней. Общее количество вызовов отрисовки снижается на 60%.На этом первая часть серии статей завершается. Во второй части инженер по разработке технологий Лу Крамер расскажет о неоднородном индексировании ресурсов на PC и, в частности, в картах AMD.