пятница, 13 апреля 2012 г.

Batch Drawing

Я решил потратить время и написать отдельную демку, которая бы показывала преимущество от рисования сцены батчами над традиционными методами рисования, в Direct3D 11. Общая идея уже была описана ранее, теперь дело стало за концептом.

Суть теста такова: он должен визуализировать большой массив объектов, каждый из которых имеет свою уникальную трансформацию. Программа должна реализовывать несколько способов отрисовки, чтобы можно было оценить прирост быстродействия по отношению к методу "в лоб". В идеале оптимизированный вариант должен всегда упираться в GPU.


Для простоты я взял один геометрический объект - Box, и размножил его в массив. На самом деле, как я уже показывал, можно использовать самую разную геометрию (если топология примитивов постоянна), но в данном случае это не принципиально. Вершины и нормали бокса хранятся в формате half, индексы - в ushort, при объединении в один буфер индексы переводятся в формат uint. Массив боксов имеет размеры 32x32 (1024 объекта), т. к. это максимальное количество матриц размерностью 4x4, которое может быть размещено в одном буфере констант (ограничение SM 4.0). Можно взять и больше, использовав, например, Buffer из SM 5.0 или даже текстуру для хранения констант, но я остановился на самом простом варианте. 

Массив боксов рисуется дважды - один раз в Z-only pass, затем в diffuse pass, т. е. делается 2K draw call-ов. Базовый способ отрисовки - по-объектный; оптимизированный - без изменения render states; наиболее быстрый - одним вызовом ID3D11DeviceContext::DrawIndexed(). Все они уже были описаны мною ранее.

После того, как была написана первая рабочая реализация, стало понятно, что в оптимизированных вариантах программа упирается уже не в API, а в расчёт матриц трансформации, т. е. всё равно в CPU. Например, в цикле для каждого объекта нужно рассчитать WVP матрицу и записать её в буфер констант:

XMMATRIX rot = build_rotation(p, y, r);
XMMATRIX trans = build_translation(x, y, z);
XMMATRIX world = rot * trans;
XMMATRIX world_view_proj = world * precomputed_view_proj;

Этот код CPU-bound, несмотря даже на то, что используются SSE-интринсики из XNA Math. Его оптимизация превратилась в увлекательное занятие. Первое, что можно тут сделать - не перемножать матрицы поворота и переноса, а просто записать вектор переноса в четвёртую строку матрицы поворота. Второе - не умножать на матрицу вида-проекции, а записывать в буфер констант сразу матрицу преобразования в мировые координаты. Матрица вида-проекции записывается в отдельный буфер констант в начале кадра, а в вершинном шейдере вместо одного умножения на матрицу последовательно выполняется два. Далее, в XNA Math матрица поворота из трёх углов на самом деле строится в два приёма: сначала строится кватернион поворота, а затем он конвертируется в матрицу 4x4, что неоптимально. Я решил записывать в константный буфер этот самый кватернион + вектор переноса, а в шейдере умножение на матрицу заменить умножением на кватернион и сложением с вектором переноса. Матрица 4x4 занимает 64 байт, тогда как пара кватернион/вектор занимает 32 байт, т. е. через шину PCI передаём в два раза меньше данных. В шейдере умножение на кватернион занимает 8 инструкций dxasm против 4-х для матрицы. Т. е. мы последовательно разгружаем CPU за счёт увеличения нагрузки на вершинный шейдер, но т. к. изначально программа CPU-bound и нужно сместить workload, то все эти методы оправданы. К слову, общее кол-во вершин в демке невелико и раздувшийся вершинный шейдер тут слабо влияет на скорость работы. В реальном случае со сложной геометрией потребление ресурсов GPU конечно будет расти быстрее.

После всех оптимизаций расчёт трансформаций перестал быть "бутылочным горлышком", даже использование кватернионов не дало значительного эффекта :) Это значит, что программа стала "упираться" в пересылку констант на GPU. В кавычки взято потому, что затрачиваемое время никак нельзя назвать значительным для такого кол-ва констант. Наиболее ощутимым эффект от оптимизации проявился на слабых системах, для примера возьмём мой ноутбук (процессор Intel® Celeron® M Processor ULV 723, видео AMD Radeon HD 4330):

per-object: 9.3 ms
w/o render state alternation: 1.85 ms
one batch: 1.33 ms
one batch w/ quaternions: 1.33 ms

Т. е. получилось ускорить вывод почти на порядок! Между вторым и третьим способом различия в производительности заключаются в десятых долях миллисекунды, что свидетельствует о том, что DIP cost в рантайме/драйвере незначителен в случае отсутствия изменений на конвейере. На мощных системах (процессвор Intel Core i5, видео GeForce GTX 560) выигрыш не так заметен, т. к. изначально даже неоптимизированный вариант занимает меньше 2 ms - нужна более сложная сцена:

per-object: 1.32 ms
w/o render state alternation: 0.34 ms
one batch: 0.19 ms
one batch w/ quaternions: 0.17 ms

Ссылка на демо: batchdraw.zip

Доступны два exe, один выполняется в окне, другой - в полноэкранном разрешении 1280x720.
Отрисовка текста сжирает большую часть времени, поэтому его можно прятать клавишей T.

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

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