Ад визуализации 1.1:
- Книга 1: Обзор
- Книга 2: Проблемы
- Книга 3: Решения
- Книга 4: Заключение
Добро пожаловать во вторую книгу! Здесь мы изучим некоторые проблемы, которые могут возникнуть во время процесса визуализации. Но, для начала, немного практики:
Знать о проблеме — полезно. Но действительно прочувствовать проблему гораздо лучше для понимания. Давайте попробуем поставить себя на место CPU/GPU.
Эксперимент
Пожалуйста, создайте 10000 маленьких файлов (например, размером 1 КБ каждый) и скопируйте их с одного жесткого диска на другой. Эта операция займет продолжительное время, хотя размер данных составляет всего 9.7 МБ.
Теперь создадим один файл размером 9.7 МБ и скопируем его тем же образом. Эта операция выполнится значительно быстрее!
Почему? Ведь размер данных одинаков!
Это правда, но каждая операция копирования состоит из множества вещей, которые надо сделать, например: подготовить файл к перемещению, выделить память, перемещать головки чтения/записи диска вперед-назад… Все это — накладные расходы для каждой операции записи. Как вы могли испытать на своей шкуре, эти накладные расходы огромны, если вы копируете множество маленьких файлов. Визуализация множества полигональных сеток (то есть, выполнение многих команд) значительно сложнее, но ощущается похоже.
Теперь давайте рассмотрим худший случай, который может возникнуть в процессе визуализации.
Худший случай
Иметь множество маленьких полигональных сеток — плохо. Если они используют разные параметры материалов, то все становится еще хуже. Но почему?
1. Множество полигональных сеток
Графический процессор может рисовать быстрее, чем центральный процессор отправлять команды.
Основная причина уменьшения количества Draw Call'ов заключается в том, что графическое оборудование может изменять и визуализировать треугольники значительно быстрее, чем вы передавать их. Если отправлять небольшое количество треугольников на каждый вызов, то вы окажетесь полностью связанными производительностью CPU, а GPU по большей части будет находиться в режиме ожидания. CPU не будет в состоянии «кормить» GPU достаточно быстро. [f05]
Ко всему прочему, каждый Draw Call производит некоторые накладные расходы (как было сказано выше):
Существуют накладные расходы на уровне драйвера всякий раз, когда вы делаете вызов API, и лучший способ уменьшить их — вызывать API как можно реже. [a02]
2. Множество Draw Call'ов
Один из примеров таких дополнительных расходов — это буфер команд. Вы помните, что CPU наполняет буфер команд, а GPU читает его? Да, им приходится сообщать об изменениях и это также создает накладные расходы (происходит изменение указателей чтения/записи, прочитать подробнее вы можете здесь)! По этой причине, может оказаться лучше не передавать команды по одной, а сначала заполнить буфер и передать целый блок команд графическому процессору. Это увеличивает риск того, что GPU придется ждать пока CPU закончит строить блок команд, но при этом уменьшает расходы на коммуникацию.
У GPU (к счастью) есть много вещей, которые надо делать, пока CPU составляет новый буфер команд (например, обработка предыдущего блока). Современные процессоры могут заполнять сразу несколько буферов команд независимо друг от друга, а после последовательно передавать их GPU.
Выше был описан только один пример. В реальном мире не только CPU, GPU и буферы команд переговариваются между собой. API (DirectX, OpenGL), драйвера и множество других элементов включены в этот процесс, что не делает его проще.
Мы обсудили только случай с многими полигональными сетками, которые используют один и тот же материал (Render State). Но что случится, когда мы захотим визуализировать объекты с разными материалами?
3. Множество полигональных сеток и материалов
Сброс конвейера.
Изменяя состояние, иногда приходится частично или полностью сбрасывать конвейер. По этой причине изменение шейдера или параметров материала может оказаться очень дорогой операцией […] [b01]
Вы думали, хуже уже не будет? Итак… если вы используете разные материалы с разными полигональными сетками, вы не можете сгруппировать команды визуализации. Вы задаете Render State для первой сетки, командуете отобразить ее, потом задаете новый Render State, отправляете следующую команду визуализации и так далее.
Я покрасил команду «Change State» в красный, так как а) она дорогая и б) для читабельности.
Установка значений Render State'а иногда (не всегда, зависит от параметров, которые вы хотите поменять) влечет сброс всего конвейера. Это значит, что каждая полигональная сетка, которая обрабатывается в данный момент (с текущим Render State'ом), должна быть отображена до того, как можно будет приступить к визуализации следующей (с новым Render State'ом). Это выглядит как на видео выше.
Вместо того, чтобы брать огромное число вершин (например, комбинируя несколько сеток с одинаковым Render State'ом. Эту оптимизацию я объясню позже), отображается небольшое количество перед операцией смены Render State'а, что очевидно плохо.
Между прочим: Так как CPU требуется некоторое минимальное время для установки параметров Draw Call'а (вне зависимости от размера полигональной сетки), можно считать, что нет никакой разницы в отображении 2 или 200 треугольников. GPU — чертовски быстр и пока CPU подготовит новый Draw Call, треугольники уже станут новоиспеченными пикселями на экране. Конечно, это «правило» изменится, когда мы будем говорить о комбинировании нескольких маленьких полигональных сеток в одну большую (мы рассмотрим это позже).
Мне не удалось найти свежие данные о количестве полигонов, которые можно визуализировать «бесплатно» на современных графических картах. Если вы знаете что-либо об этом или делали недавно какие-либо измерения, пожалуйста сообщите мне!
4. Полигональные сетки и мультиматериалы
Что если полигональной сетке назначен не один материал, а два или более? В основном, сетка рвется на несколько кусков, а затем по частям «скармливается» буферу команд.
Конечно же, это влечет дополнительные Draw Call'ы на каждый элемент сетки.
Надеюсь, мне удалось дать вам беглое представление о том, что плохого в большом количестве полигональных сеток и материалов. В следующей книге мы рассмотрим некоторые решения, пусть все это и выглядит ужасно. Но существуют прекрасные игры, которые доказывают, что описанные выше проблемы как-то удалось преодолеть.
Конец
[a02] GPU Programming Guide GeForce 8 and 9 Series
[b01] Real-Time Rendering: Стр. 711/712
[f05] Why are draw calls expensive
Автор: AndreyMI