Arseny Kapoulkine:
Approaching Zero Driver Overhead in OpenGL (Presented by NVIDIA)
Speakers: Cass Everitt (NVIDIA), Tim Foley (Intel), John McDonald (NVIDIA), Graham Sellers (AMD)
На этом GDC были интересные доклады про API, я на большинстве из них был так что могу рассказывать все в правильном порядке и с кросс референсами.
Начнем с OpenGL!
Я расскажу поэтому короче чем предыдущие.
OpenGL - большое и сложное API, драйвер должен традиционно выполнять кучу работы,
чтобы отрисовать то что вам надо отрисовать.
Это проблема DIP cost, говорят что в OpenGL он меньше (и он таки меньше) чем в D3D, но он все равно есть и немаленький.
Давайте бороться с DIP cost радикальными методами - делать меньше DIP-ов!
Стандартно выясняется (см. мой рассказ http://public.closedcircles.com/posts/gdc2014-effecient-work-submission-direct3d) что если стейт не менять между draw calls то может стать лучше.
Давайте сфокусируемся на ряде проблем которые заставляют нас менять стейт или неэффективно использовать драйвер и их порешаем.

Сначала научимся эффективно заполнять буферы данными на GPU - это нам пригодится дальше

Базовый пример - динамическая геометрия; то же самое работает для буферов с другими данными, типа uniform buffer objects.
Можно использовать glMapBufferRange с различными флажками.
Проблема во-первых в том что это не бесплатно - драйвер на каждую Map операцию делает нетривиальные действия (Map с INVALIDATE_BUFFER_BIT дорогой т.к. драйвер должен создавать новый буфер, например)
Во-вторых в том, что если пытаться просить драйвер не делать нетривиальных действий и использовать Map с флагом UNSYNCHRONIZED, то на драйверах которые выполняют основную работу в отдельном потоке.
Как это ни парадоксально, UNSYNCHRONIZED_BIT заставляет главный тред синхронизироваться (ждать) с драйвер тредом.
Это случается конкретно в драйверах NVidia; как и всякая другая синхронизация, это дорого и может сильно ухудшить общую производительность.
В OpenGL есть новый способ - можно создать буфер со специальными флажками, которые позволяют сделать glMapBuffer один раз после создания, и никогда не делать unmap.
Т.е. вы просто записываете данные по указателю полученному один раз когда-то и потом делаете draw.
Разумеется, можно случайно записать данные которые читает в данный момент GPU - результаты в таком случае не определены.
Чтобы такого не произошло, нужно самому синхронизироваться с GPU - для этого можно использовать fences.
Это объект который используется так - после того как вы отдали все команды на отрисовку фрейма, вы "устанавливаете" fence; когда вы хотите переиспользовать соответствующий участок буфера вы ждете, пока GPU не закончит обрабатывать команды и не дойдет до этого fence.
На этой конструкции можно реализовать безопасный ring-buffer в который CPU будет писать данные, а GPU - читать.
Рекомендуется ringbuffer делать размера в 3 раза большего чем вы в среднем туда пишете данных.
Рекомендуется написать helper который помогает работать с этим ringbuffer - вот примерно как он может выглядеть https://github.com/nvMcJohn/apitest/blob/master/src/framework/bufferlock.cpp

Теперь давайте убирать state setup

  • 1. Текстуры надо как-то эффективно отдавать на GPU.
Можно использовать bindless текстуры - это механизм при котором вы получаете хендл (64-битное число) на текстуру, который как угодно передаете в шейдер и в шейдере читаете из текстуры как обычно.
Если их нет, можно использовать массивы текстур - для каждого размера текстур создаете массив и в него засовываете текстуры.
Сколько слоев в таком массиве делать неочевидно, на помощь приходят виртуальные текстуры.
В GL есть виртуальные текстуры примерно как в D3D 11.2 (см. мой рассказ позавчера)
Только tier1 - нельзя узнать результаты выборки с т.з. наличия страниц (tier2 есть только в AMD-specific расширении)
Причем отмечу вообще что MS гораздо лучше задизайнила расширение...
Во-первых, у MS размер тайла фиксирован спецификацией.
А в GL любой, зависит от вендора.
Во-вторых, у MS маппинг (page table) контролирует разработчик.
А в GL - драйвер, и нельзя задать пул.
В итоге выглядит так что для ряда приложений решением MS пользоваться проще.
Ну вот, теперь делаем огромный виртуальный массив текстур, и слои в нем загружаем в память только те которые нужны.
Массивов все равно нужно много - по одному на размер и формат текстуры.
Можно комбинировать фичи описанные выше и получить sparse (virtual) bindless texture arrays.
Зачем это нужно на практике я не очень понял - вроде bindless хватает. Впрочем bindless не всегда есть.
  • 2. Текстуры отдали, теперь давайте притворимся что у нас один и тот же шейдер и остальной сетап пайплайна и попробуем смержить все остальное.
Смержим сначала геометрию в огромные VBO, их переставлять больше не будем.
Смержим теперь форматы вершин, чтобы настраивать потоки вершин один раз.
Чтобы мержить геометрию в огромные VBO, будем кстати использовать glDrawElementsBaseVertex - не прошло и тысячи лет, а D3D9 фичу добавили в GL.
Теперь у нас последовательность draw команд для одного шейдера выглядит так -
for (...)
{
memcpy(uniformbufferpointer + offset, uniformdata)
drawelements.
}
(мы используем persistent mapping описанный в начале)
Теперь воспользуемся MultipleDrawIndirect фичей - она позволяет нам создать буфер в котором записаны параметры draw call (типа base vertex index и index count) и запустить много draw calls.
Этот буфер можно генерировать на CPU/GPU.
Ну и константы мы понятно зальем все сразу.
Как найти нужные константные данные?
Есть gl_DrawIDARB в шейдере,
который рассказывает вам индекс draw call в цепочке multi draw indirect.
Иногда его нет и его можно эмулировать.
В итоге -- если шейдер один и тот же и отличаются параметры и текстуры,
то можно залить все uniforms в огромный буфер.
В тот же буфер залить uint64 хендл на bindless текстуру,
в другой огромный буфер залить параметры всех draw calls,
а потом один раз сказать MultiDrawIndirect.
Эти два буфера можно генерировать на GPU - полезно, например, если на GPU делать куллинг.
Там есть детали что делать когда нет расширений итп.
Плюс есть код который демонстрирует эти техники на синтетических примерах и меряет 10+ кратное ускорение.
Подробности в слайдах по ссылке в начале треда, плюс на github https://github.com/nvMcJohn/apitest/
В коде apitest и в презентации есть сравнение по скорости с D3D11 в том числе.