Использование форка Meta RenderDoc для оптимизации приложения, часть 1
В этом руководстве рассматриваются несколько основных сценариев использования RenderDoc Meta Fork, которые можно использовать для оптимизации вашего приложения. Они помогут убедиться, что используются наиболее эффективные способы отсроченного тайлового рендеринга, используемые устройствами Meta Quest, которые проведут по нескольким сценариям, чтобы убедиться, что методы рендеринга на базе центрального и графического процессоров выполняются максимально эффективно.
Подтверждение инстансированного стереорендеринга
Из-за особенностей VR-рендеринга важно убедиться, что вызовы отрисовки настроены оптимально. В фоновом режиме движок отображает оба глаза по отдельности. Каждый глаз имеет собственные матрицы модель-вид-проекция, определяемые расстоянием между осями (IAD), определяемым настройками линзы. Многие пользователи знают о настройке IPD (межзрачкового расстояния), которая определяет расстояние между осями. В коммерческих движках это обычно происходит автоматически, но вам следует убедиться, что у вас настроены правильные настройки. Ниже представлен краткий обзор всех типов вариантов рендеринга.
Single Pass Rendering (Однопроходный рендеринг) — для каждой сетки в базовом проходе:
- рисуется левый глаз (1 вызов отрисовки);
- рисуется правый глаз (1 вызов отрисовки).
Multi-Pass Rendering (Многопроходный рендеринг) — для каждой сетки в базовом проходе:
- рисуется левый глаз (1 вызов отрисовки) и снова для каждой сетки в базовом проходе:
- рисуется правый глаз (1 вызов отрисовки).
Instanced Stereo Rendering/Multi-View (Инстанцированный стереорендеринг/многовидовой) — для каждой сетки в базовом проходе:
- рисуются оба глаза (1 вызов отрисовки).
Как можно заметить, инстанцированный стереорендеринг/многовидовой — это самый эффективный и оптимальный вариант, поскольку он вдвое сокращает общее количество вызовов отрисовки в базовом проходе.
Ниже перечислено несколько актуальных ресурсов, которые помогут изучить больше информации о мультипросмотре.
Убедитесь, что временные буферы не используются
Используйте средство просмотра текстур для проверки выходных/рендеринговых текстур, чтобы убедиться, что имя ресурса обозначено как RTTextureArray, а не TempBuffer, ColorBuffer или как-то ещё. Для применения MSAA и/или получения сохраненного фиксированного фовеального рендеринга (FFR) необходимо выполнить запись непосредственно в свап-цепочку текстур. Это также гарантирует, что вы случайно не определите промежуточную цель рендеринга/временный буфер, что сохранит фиксированные затраты на разрешение в 1–1,5 мс на графическом процессоре.
Текстура XR [#] — это то, что видно в качестве имени вывода данных текстуры при записи в свап-цепочку текстур. В этом примере вы увидите, что выбранный вызов отрисовки находится под аннотацией под названием Final Blit Pass. Это означает, что вы берете ранее созданную мишень рендеринга и используете ее в качестве временного буфера, а затем копируете ее в свап-цепочку текстур. Вы оплачиваете дорогостоящее разрешение на ГП, а затем платите за каждый пиксель, чтобы отобразить его только на свап-цепочку текстур.

На этом снимке экрана показан выделенный вызов отрисовки базового прохода из того же кадра, который был показан ранее. Соответствующая выходная цель рендеринга показывает метку этого: _CameraColorTexture. Это означает, что движок сгенерировал промежуточную текстуру рендеринга, которая будет использоваться в качестве входного ресурса для последующего копирования/блитирования, что подразумевает затраты на разрешение, без сохранения пиксельного шейдера фиксированного фовеального рендеринга и упуская улучшения качества MSAA. Это показывает, что вам всегда следует помнить о мишенях рендеринга, чтобы убедиться, что вы выполняете рендеринг только за один проход непосредственно в свап-цепочку текстуры.
На этом снимке экрана можно увидеть, что маркеры отладки в Unity явно аннотируют процесс разрешения карты теней с помощью маркера Resolve Shadows. Также можно просмотреть аннотацию Final Blit Pass, чтобы увидеть копирование временного буфера цвета в свап-цепочку текстуры.
Подтвердите теневые отражатели, приемники, каскадные уровни и разрешения теневых карт
Для всех сеток, отбрасывающих и/или получающих тени, потребуется дополнительный вызов отрисовки, который записывает в буфер глубины данные об ориентации источника света. Хотя для этих отрисовок не выполняется фрагментный шейдер (то есть они намного дешевле на ГП, чем типичная отрисовка с затенением), всё равно существуют накладные расходы как на CPU (увеличение количества вызовов отрисовки), так и на ГП (вершинные шейдеры, проецирующие всю геометрию сетки и разрешающие карту глубинных теней).

На этом скриншоте показано, как выглядит карта теней в RenderDoc. Вы заметите, что в вашем проходе теней будет временный буфер глубины, который вы позже будете использовать для теней вашей сцены. Важно проверить общее разрешение карты теней и количество вызовов отрисовки, связанных с каждым каскадом. Почти во всех случаях более близкие каскады будут иметь меньше связанных с ними вызовов отрисовки, поскольку расстояние до камеры увеличивается с каждым дополнительным каскадом. Также обратите внимание на раздел разрешения теней, выделенный оранжевым цветом. Это фиксированная стоимость, взимаемая при сохранении временного буфера во внешней памяти. Если вам необходимо использовать тени, обязательно установите максимально низкое разрешение и количество дополнительных вызовов отрисовки.
Проверка ГП-экземпляров общих сеток
Клонирование ГП — это функция графического API/уровня драйвера, которая позволяет направлять один вызов отрисовки для рендеринга нескольких сеток. Существует несколько правил, которые необходимо соблюдать, в том числе совместная передача одного и того же состояния конвейера и входных данных сетки. Если у вас есть сетка, которая используется несколько раз за кадр (пули, декор, частицы, листва и т. д.), вы можете существенно сократить количество вызовов отрисовки, убедившись, что используете клонирование для своих сеток. Обратите внимание, что это не снижает нагрузку на графический процессор, поскольку вам всё равно придется проецировать каждую сетку.
Каждый раз, когда вы выполняете клонированный вызов отрисовки, все сетки получают свои собственные идентификаторы экземпляра, который можно использовать в шейдерах. Это позволяет связывать идентификатор каждого экземпляра сетки с информацией о свойствах, такой как индексы текстур, цвета или что-либо ещё. Примером может служить визуализация больших толп в гоночной игре. Вы можете использовать клонирование, чтобы нарисовать каждого человека в зале в рубашке разного цвета, чтобы действительно создать ощущение огромной разнообразной аудитории без дополнительных затрат ресурсов CPU на отправку отдельного вызова отрисовки для каждой сетки. В этом простом примере вы не будете использовать сетки со скинами, но вы можете клонировать сетки со скинами, выполняя операции скиннинга/анимации на графическом процессоре. Это более сложная в реализации техника, которая также имеет свои плюсы и минусы. Идея состоит в том, чтобы создавать экземпляры Т-образных сеток, совместно используемых для рисования, а затем использовать идентификатор экземпляра для поиска и вычисления меток времени и информации о позе. На этом поиск смешивания форм завершается, и выполняется скиннинг в вершинном шейдере. В качестве альтернативы можно заранее выполнить пакетную обработку всех операций смешивания форм и скининга в вычислительном шейдере, а затем использовать идентификатор экземпляра для поиска местоположений соответствующих данных для текущей отрисовываемой сетки.
Кстати, именно через клонирование ГП работает клонированный стереоскопический и многовидовой рендеринг. Каждый вызов отрисовки с использованием этого метода отправляет два экземпляра на сетку. Идентификатор экземпляра каждой из двух сеток сопоставляется для отрисовки с соответствующим глазным буфером, который вершинные шейдеры используют для индексации в массиве матриц проекций вида модели для правильного проецирования сеток в соответствующее местоположение глаза.

На скриншоте выше можно видеть, что вместо двенадцати отдельных вызовов glDrawElements(36) видно 4x glDrawInstanced. Причина, по которой glDrawElementsInstanced(36, 5) не отрисовал все кубы за один раз, заключается в том, что некоторые кубы нарушали правила пакетной обработки в Unity. Вы можете выяснить причину, по которой они не были сгруппированы, используя инструмент Frame Debugger в Unity, чтобы убедиться, что вызывается glDrawElementsInstanced(36, 12), в результате чего общее количество отрисовки кубов сводится к одному вызову отрисовки.

На этом скриншоте Unity Frame Debugger выбран второй экземпляр вызова отрисовки, и Unity сообщает нам, что на кубы влияют различные зонды отражения в разделе Почему этот вызов отрисовки нельзя объединить с предыдущим. Чтобы убедиться, что все они завершены за один вызов отрисовки, для всех этих кубов следует использовать один отражающий зонд. Инструмент Frame Debugger устраняет необходимость догадок из уравнения, но вы также можете прийти к такому выводу, проведя небольшое исследование на вкладке "Состояние конвейера" в RenderDoc. Просто проверьте и сравните ресурсы входных данных между обоими вызовами отрисовки.
Подтверждение Renderque и Z-сортировки
В большинстве ГП имеется метод оптимизации, который может использоваться оборудованием для отклонения скрытых непрозрачных пикселей, если они прорисовываются спереди назад. Например, в VR в большинстве сценариев руки должны отображаться первыми, если они непрозрачны. Если вы записываете данные в буфер глубины во время прорисовки руки (предположительно, одного из ближайших к камере объектов) и назначаете z-тест на отклонение пикселей за ней, то оборудование достаточно умно, чтобы отказаться от выполнения пиксельного шейдера для пикселей проецируемой геометрии, которые оказываются за рукой. Пиксельные шейдеры обычно оказываются самой тяжелой частью конвейера рендеринга, если используются разумные подсчеты геометрических фигур и не делается ничего сложного в вершинном шейдере.
Важно отметить, что поскольку панели Meta Quest имеют высокое разрешение и приходится рисовать обоими глазами, сохранение здесь удваивается. В некоторых ситуациях для листвы, имеющей дорогие пиксельные шейдеры, может оказаться полезным предварительный проход глубины, когда устанавливается буфер глубины для альфа != 0 пикселей, а затем следует проход цвета, настраивая тест глубины на равное значение. Эксперимент с обоими методами может помочь вашему приложению.
Ниже на фото представлен пример.
На этом скриншоте показана конечная мишень рендеринга в проблемной сцене. На первый взгляд всё выглядит хорошо, но внутри здания отображаются объекты, которые не только используют ненужные вызовы отрисовки и загружают конвейер на стороне CPU, но и занимают ресурсы графического процессора, проецируя все геометрические фигуры внутренних объектов и затеняя всё, что было спроецировано без необходимости. На следующем скриншоте можно подробно рассмотреть, что на самом деле происходит в этом неэффективном сценарии.
Аннотированные отрисовки представляют собой небольшую часть всех ненужных вызовов отрисовки, которые были отправлены на ГП. Они обработали и затенили все эти геометрические фигуры без всякой на то причины. Если бы CPU предпринял некоторые превентивные меры, чтобы убедиться, что они отсеиваются, ГП не пришлось бы их обрабатывать. Подробнее об исключении отсеивания читайте далее.
Рассмотрим аналогию: вы нанимаете маляров для покраски вашего дома за почасовую оплату. Если бы они сначала решили покрасить всю отделку в красный цвет, а затем покрасили весь дом в один сплошной цвет, вы бы расстроились и смутились, если бы вам пришлось платить им за время, потраченное на покраску отделки в красный цвет, несмотря на то, что вы дали им указание покрасить дом в один цвет. В мире игр и графики обработка ГП — это ресурс, который можно было бы потратить на что-то другое, например на более детальные шейдеры или материалы. Для сравнения, на следующем скриншоте показан тот же кадр, но с выбранным вызовом отрисовки, который находится дальше в порядке заказа.

Как уже говорилось, при выборе вызовов рисования в журнале событий в RenderDoc кадр выстраивается перед глазами в хронологическом порядке. При выборе этого вызова отрисовки, всего через несколько вызовов после отрисовки на предыдущем скриншоте, можно увидеть кучу моделей стен, закрывающих всё, что было ранее отрисовано. Это означает, что все те вызовы отрисовки, которые вы ранее отправили, выполнили свои пиксельные шейдеры и были перерисованы (например, красная отделка на доме). Если бы вы нарисовали стены и потолки окружающей среды до того, как эти объекты были внутри, вы бы установили буфер глубины на глубину сеток стен и сохранили бы стоимость шейдера фрагмента каждого пикселя, который был перерисован, поскольку оборудование могло бы определить, что более близкий пиксель его перекрывает. Если ваша игра не может позволить себе отбраковку окклюзии, вы всё равно можете сохранить время на ГП, используя более оптимальный алгоритм сортировки, подобранный специально для вашей игры. И Unity, и Unreal позволяют переопределять место определенных отрисовок и материалов в очереди рендеринга.
Подтверждение формата и разрешения текстур
С помощью RenderDoc вы можете подтвердить, что разрешение входных и выходных данных текстур не слишком высокое, что предоставляются MIP-карты, формат сжатия соответствует вашим ожиданиям, количество текстур, сэмплируемых за одну отрисовку, и многое другое. Отслеживать информацию о текстурах для всех текстур в редакторах движка может быть сложно, поэтому это хороший способ проверки, сохранения памяти и поддержания эффективности.
Давайте рассмотрим входную текстуру, чтобы увидеть детали:
На этом скриншоте выбран вызов отрисовки Ogre, а также входной текстуры Albedo на вкладке "Входные данные". Видно разрешение, формат сжатия и количество уровней MIP для каждой входной текстуры. Видно, что для входных текстур используется формат сжатия текстур ASTC, который рекомендся использовать для достижения оптимального качества и размера.
Форматы текстур с высоким динамическим диапазоном (HDR) в большинстве случаев не следует использовать в Meta Quest. Для HDR требуется временный буфер с форматом, отличным от формата свап-цепи текстуры, обычно с форматом R11G11B10_FLOAT, а не нормальным R8G8B8A8_SRGB. Проблема заключается не в битах на пиксель, поскольку они оба 32-битные, а в стоимости копирования и преобразования временного буфера из HDR в нормальный формат, когда кадр уже готов. Эффекты и вычисления HDR обычно также обходятся намного дороже из-за требования более высокой точности вычислений с плавающей точкой. Опять же, при использовании временного буфера вы не получите преимуществ фиксированного фовеального рендеринга или MSAA, поэтому вы можете понести дополнительные потери результативности и/или качества. Вы можете подтвердить, соответствует ли ваш таргетированный рендеринг спецификации HDR, убедившись, что вы записываете в цепочку обмена текстуру с именем XR Texture [#] в формате R8G8B8A8_SRGB, как показано на сравнительных скриншотах ниже.
