Миллион батчей в секунду на Xbox360

Во время очередного витка оптимизации на Xbox360, захотелось очень дешевых по производительности батчей - т.к. батчей в кадре было достаточно много, а CPU времени они кушали еще больше, при тысяче батчей в кадре тратилось 20-25 ms на установку констант и вызов DP. CPU лимит в 40-50 FPS явно не устраивал.

Как потом выяснилось часть оверхеда была от использования D3D Effects в качестве шейдерной системы. Большое количество Load Hit Store и двойное копирование всех констант, были одной из проблем.

Итак, задача: 30 тысяч строк написанных и отлаженых шейдеров которые нельзя менять и желание иметь в runtime подобие PS3 libGCM и полностью контролировать генерацию command buffer.
Я начал рыть в сторону PrecompiledCommandBuffer т.к. там явно была возможность записать CommandBuffer и потом отправить его в GPU. К сожалению, изменять параметры у записанного CB можно очень ограниченно, поэтому данный метод не подошел.

В результате экспериментов я получил дамп памяти GPU буфера и стал его разбирать ручками. К счастью, Xenos это развитие чипа ATI R500, а на R500 документация открыта (спасибо AMD/ATI). Очень быстро был готов парсер CB, который выдавал полную информацию, о том что лежит в буфере.

Выяснилось, что часть операций которые можно делать из D3D совсем по другому мапяться в железо, да и Runtime D3D сохраняет зеркальные стейты GPU, что тоже не бесплатно. Также паралельно выяснилось, что размер CB на каждом батче слишком большой, т.к. частично шейдеры эмбедились в CB и устанавливалось очень много констант (они всегда эмбедятся в буфер).

Как результат всех эксперементов я написал компилятор на входе берущий D3D Effect файл и генерирующий из него кусочки CB (кусочки что бы можно было не сабмитить уже установленные в GPU данные), так же runtime ремапинг адресов, сабмишн данных в GPU и всякий дополнительный код.

Теперь рендер батча стал просто заполнением буфера памяти, поборовшись Load-Hit-Store на заполнении памяти и сделав кеширование CB в системной памяти (общая память CPU и GPU на X360 и на PC в режиме WriteCombined, что отключает все кеши и ведет к разным пенальти из за непоследовательной записи) я получил хороший прирост производительности.

После всех основных оптимизаций, батчи стали стоить до 4ms на кадр (вместо 25ms), профайлинг показывал узкое место на доступе к памяти (констант было от 200 до 400 на батч). У ATI GPU есть очень приятная фича, на вход микрокода вершинного шейдера приходит номер индекса, и сборка вершины происходит в шейдере с возможностью random read из потока вершин. Я переделал большинство констант на vertex stream (данные vertex stream заполнялись в другом cpu потоке) и избавился от 90% констант при отрисовке, результат: 1-2ms из секунды это расходы на отрисовку 1000 батчей, на этом и остановился.

В теории успех можно было развить дальше, т.к. можно вести отрисовку с нескольких потоков (при рендере мы просто работаем с памятью, не трогая D3D вообще, D3D нужен только для сабмишена данных в GPU) - но на текущем проекте этого не потребовалось. CPU лимит в миллион батчей в секунду кажется мне достаточным, да и PC такого не переживет - надо все таки и о второй платформе помнить.

Итого:

Было: D3D Effects + D3D runtime
0.02ms batch * 1000 = 20ms на кадр (50000 batches per second)

Стало: Своя библиотека работы с GPU:
0.001ms batch * 1000 = 1ms на кадр (1000000 batches per second)

Leave a comment

Your comment