Arseny Kapoulkine:
Ну вот а теперь про еще одно главное событие GDC!
Значит, в среду я был на двух докладах Mantle,
и там рассказывали про проблему DIP cost.
Драйвер делает за девелоперов много работы,
и все это выливается в плохую производительность и в плохое масштабирование API с т.з. многоядерности.
Вот например есть такая проблема, и еще такая, и еще вот такая.
Но их можно решить, изменив вот это в API, и вот это и вот это,
введя концепт #1 и концепт #2 итп.
И будет счастье.
И я сидел и кивал головой.
А в четверг с утра был доклад про DirectX12 на котором рассказали что - шок! - DirectX12 будет.
Но зачем, спросите вы?
Все просто - драйвер делает за девелоперов много работы.
И все это выливается в плохую производительность и в плохое масштабирование API с т.з. многоядерности.
Вот например есть такая проблема, и еще такая, и еще вот такая.
Но их можно решить, изменив вот это в API, и вот это и вот это,
введя концепт #1 и концепт #2 итп.
И будет счастье.
И я сижу и понимаю что:
  • 1. Список проблем - ровно такой же какой был вчера на докладе про Mantle.
  • 2. Список решений - ровно такой же какой был вчера на докладе про Mantle.
  • 3. Даже имена решений совпадают чуть более чем полностью.
Поэтому давайте я расскажу сразу про все доклады про Mantle и DirectX12 разом :)
Итак, значит проблема которую мы решаем - есть CPU-bound приложение,
которое делает много draw calls.
Через это генерирует много работы драйверу и тормозит.
Плюс еще иногда драйвер создает непонятно откуда взявшуюся работу которая неочевидна для приложения, но критична для того чтобы приложение работало корректно и быстро с т.з. GPU.
При этом много draw calls это прекрасно т.к. позволяет нам рендерить большие и богатые контентом миры.
Как ясно из презентации про OpenGL, эти проблемы можно пытаться решать средствами текущего API, фактически вынося больше работы на GPU.

Однако возникает вопрос - а почему тормозят draw calls?

Есть несколько проблем, давайте их осветим.
Если свести все проблемы к одной, то абстракция графического API течет очень сильно.
Мы программируем GPU с помощью одного сплошного потока команд которые меняют кусочки стейта на GPU, обновляют данные итп.
При этом тот факт что GPU работает одновременно с кодом на CPU от нас скрывается всеми силами.
Большое количество абстракций в железе на самом деле не существует или существует в совсем другом виде.
  • Например, в железе не существует форматов вершин, вместо этого существует вершинный шейдер который умеет читать данные из VB и декодировать форматы.
  • Например, в железе не существует слотов для текстур которые доступны в пиксельном шейдере.
  • Например, в железе не существует стейт объекта со всем стейтом блендинга в виде ID3D11BlendState.
Попытки распараллелить работу с одним сплошным потоком команд кончаются страшным.
Вот что примерно происходит в драйвере с учетом всего этого:
Приложение сабмитит набор команд вида "установи PS", "установи blend state", "поменяй текстуру номер 7 на вот эту", "сделай draw".
  • Во-первых, в железке нет VS или например blend state.
Вместо blend state и rasterizer state есть три каких-нибудь других стейт блока которые пересекаются и с blend state или с rasterizer state,
поэтому нельзя сгенерировать команду соответствующую установке blend state,
поэтому драйвер запоминает весь стейт и помечает его флажками dirty, а в момент draw он начинает генерировать настоящие команды.
  • Во-вторых, в железке нет input layout, а еще от некоторых вещей (типа формата render target!) может зависеть байткод PS,
поэтому драйвер в момент draw проверяет, есть ли у него в кеше собранный кошерный VS и кошерный PS.
И если нет - компилирует новый.
Компилирует не из HLSL, конечно, но например транслирует заново DX байткод.
Если в кеше все есть то все равно в том кеше приходится нужный шейдер искать (это медленно), если в кеше нет - приходится компилировать (это очень медленно и приводит к frame spikes)
  • В-третьих, в GPU много разных параллельных юнитов,
которые обладают хитрыми кешами итп.
Например, при записи пикселя в RT пиксель попадает в специальный кеш, который нужно сбросить чтобы данные были записаны в память GPU.
А например текстурный юнит из этого кеша читать не умеет,
потому что например этот кеш умеет сжимать цвет хитрым образом, а текстурный юнит не умеет его разжимать.
В итоге если следующий draw call читает текстуру которая была записана предыдущим, нужно GPU сообщить чтобы оно сбросило этот кеш.
С учетом того что все шейдеры теперь могут писать всякие данные фиг поймешь куда, получается что такие моменты в зависимости от архитектуры GPU могут возникать чуть ли не между каждым DIP,
поэтому драйвер должен в каждом DIP для каждого прицепленного ресурса проверить, не записывали ли мы его в предыдущем DIP,
и если да, то иногда сгенерировать соответствующую команду для GPU.
Кстати, в случае tiled ресурсов (виртуальной памяти) в общем случае это слишком сложно определить т.к. может быть RT который пишет данные туда же откуда их читает текстура но чтобы это понять надо прочитать табличку page mapping.
Поэтому в DX11.2 добавили новую функцию специально чтобы такие конфликты разруливать средствами приложения.
  • В-четвертых, у нас windows у которого WDDM в котором память GPU виртуализована -
это означает что можно нааллоцировать больше ресурсов чем есть видеопамяти.
Это круто т.к. ваше приложение не падает а начинает тормозить если ему памяти не хватает.
К сожалению, в момент draw call все ресурсы должны быть в видеопамяти.
Для этого с каждым draw call запоминаются все ресурсы, и в момент сабмита draw call на GPU (я не буду затрагивать детали WDDM тут) нужно сообщить менеджеру памяти что пора загрузить все ресурсы в видеопамять если они еще не.
Это тоже дополнительная работа.
  • В-пятых, GPU работает параллельно с CPU.
Когда вы делаете Map на CPU, драйвер должен узнать, используется ресурс сейчас на GPU или нет.
Если нет - можно отдать вам указатель на память, например.
А вот если да, то...
  • 1. Если есть discard флажок, то нужно проаллоцировать новый буфер.
И запомнить его в ресурсе, и еще сделать кучу всякой bookkeeping work,
чтобы каждый раз его не аллоцировать, нужно иметь из них пул итп.
  • 2. Если discard флажка нет, то в зависимости от API нужно либо подождать пока GPU не перестанет его использовать, либо сделать copy on write - аллоцировать новый буфер, скопировать туда данные из старого итп.
Собственно, откуда драйвер знает-то, используется ресурс или нет?
Ага!
При draw call надо в каждом ресурсе помечать, когда была отдана команда на GPU которая его использует,
после цепочки команд надо вставлять GPU fence,
чтобы на CPU спросить в момент Map, дошел GPU уже до fence или нет.
Все это сложно делать в общем случае - а драйвер должен работать всегда, вне зависимости от usage pattern.
Кстати, эти фенсы тоже надо менеджить...
  • В-шестых, GPU работает параллельно с CPU.
Что происходит, когда вы удаляете объект (вершинный буфер), а GPU все еще обрабатывает draw calls с его участием?
Удалять его нельзя.
Значит, при draw call надо помечать объект специальным образом, а при удалении не удалять объект сразу, а удалять его потом.
  • В-седьмых, на GPU иначе организована привязка ресурсов к слотам шейдеров.
Например, вместо 32-ух (или там 128-и) текстурных слотов.
Есть один указатель на табличку в памяти со всеми ресурсами.
Чтобы поменять там одну текстуру надо:
  • Проаллоцировать новую табличку.
  • Скопировать в нее старое содержимое.
  • Поменять там хендл на текстуру в одном слоте.
Еще табличка одна а ресурсов много,
поэтому есть маппинг между слотами API и слотами таблички.
Этот маппинг может быть разным для разных шейдеров.
Т.е. один и тот же слот в разных вариантах PS может мапиться в разные слоты таблички.
В итоге драйверу приходится делать до черта работы по ремаппингу и менеджменту табличек итп.
А для вас это выглядит как один вызов PSSetShaderResources.
Я очень извиняюсь за получасовое вступление, но оно было совершенно необходимо...
...чтобы...
...сказать...

...что так дальше жить нельзя.

А, да!
Совсем забыл!
Из-за всех перечисленных проблем не работают нормально deferred contexts.
Т.е. нельзя заранее сгенерировать command buffer так чтобы потом его бесплатно отправить на GPU,
потому что всю эту работу по менеджменту памяти, нотификации GPU и прочей ерунде делать заранее не получается.
В итоге ко всему прочему весь этот кошмарный процессинг однопоточный.
Страшными усилиями маленький кусочек удается распараллелить, но все равно выходит фигня.

Итак, со всем этим надо бороться

Можно бороться методом OpenGL - давайте скажем что это все кошмар и ужас,
но если делать десять draw calls на кадр то большинство указанных проблем не существует.
Остается пожалуй одна важная - микрокод шейдера зависит от установленного стейта,
типа пиксельный шейдер связан с форматом RT.
И драйверу приходится компилировать корректный микрокод just in time.
Если у вас три шейдера, впрочем, то проблема не стоит остро.
А если не три, и вы меняете стейт, то десять draw calls все равно не выйдет.
Этот метод понятен, и он имеет свои преимущества и свои недостатки - но это не путь джедаев.
Путь джедаев - это сохранив общую идею текущих сложных рендеров с большим количеством шейдеров, стейта, разных draw calls и так далее,
научиться их сабмитить а) быстро, б) параллельно с 8 ядер CPU.
Для этого нам придется решить все перечисленные проблемы -
методом перекладывания их на плечи разработчиков приложения.
Если кто-то думал что проблему можно решить иначе то придется с этой мыслью распрощаться - API моделирует фундаментально отличающуюся от текущих реалий картину мира.
У меня на сегодня все, пойду немного посплю - завтра я расскажу что конкретно такое Mantle и DX12 (впрочем, мне кажется с введением выше большинство из того про что писал Семен должно теперь быть понятно)
Arseny Kapoulkine:
Продолжим.
Итак, есть куча проблем, давайте посмотрим теперь что такое Mantle.
Во-первых, разберемся с state setup. Отдельные объекты стейта и шейдеры плохо соответствуют модели железа, так что введем понятие pipeline object (Mantle) и pipeline state object (DX12)
Этот объект включает в себя 99% текущего стейта пайплайна (кроме привязки ресурсов и еще некоторых мелочей)
А именно, все шейдерные объекты,
канонический стейт типа blend/rasterizer,
а также менее канонический стейт,
типа формата рендер таргетов.
Т.е. мы берем весь стейт который сейчас используется драйвером чтобы скомпилировать настоящий микрокод шейдеров,
плюс весь стейт который нужен чтобы собственно установить стейты (blend/etc.),
и кладем в специальный объект.
Этот объект надо создавать самому для каждой нужной комбинации.
Его в Mantle можно сохранять в файл кеша, в DX12 нельзя.
В DX12 часть стейта которую надо менять чаще чем обычно вынесена наружу (называется non pipeline state) - видимо, в этот раз таки обсудили с вендорами архитектуру GPU чтобы убедиться что это работает в долгосрочной перспективе с т.з. эффективности state setup.
Создавать этот стейт объект в Mantle кстати жутко дорого - http://www.slideshare.net/DICEStudio/rendering-battlefield-4-with-mantle вот тут говорят 10-60ms на каждый (???),
поэтому в BF4 его создают заранее параллельно на нескольких тредах и сохраняют в кеш на диске.
После того как стейт объект создан его можно устанавливать в пайплайн и иметь гарантированное отсутствие JIT компиляции в драйвере.
Т.е. если раньше у вас иногда в драйвере на некоторых draw calls игра повисала на десятки миллисекунд, то теперь она повисает когда вы сами создали новый pipeline объект - с чем вы можете бороться.
Во-вторых, разберемся с привязкой ресурсов.
Драйвер должен был выполнять много работы при draw call чтобы знать, когда ресурсы используются на GPU, чтобы не удалять их раньше времени, чтобы сообщать WDDM что их нужно держать в памяти итп.
Поэтому:
  • 1. Все ресурсы существуют в памяти в одной копии. Нет Map с DISCARD, нет buffer renaming и copy on write.
Есть ресурс который ссылается на кусок памяти - если вы хотите записать данные с CPU в эту память, вам надо гарантировать самим что GPU ее сейчас не читает.
Техники те же самые что и использующиеся в OpenGL для persistent mapping (см. рассказ выше)
  • 2. Приложение берет на себя ответственность не освобождать ресурсы которые используются на GPU (напомню, GPU работает асинхронно - я потом расскажу как это можно делать в графических движках использующих API)
  • 3. Приложение берет на себя ответственность сообщать GPU, когда нужно сбрасывать кеши итп чтобы предотвращать hazards (см. пример выше про кеш RT который нельзя читать при текстурном фетче)
Для этого есть понятие resource state transition (Mantle) и resource barrier (DX12)
Т.е. у вас есть вызов который отдает команду GPU, которая нужна чтобы гарантировать что действие "ресурс Х раньше использовался как рендер таргет а теперь будет использоваться как текстура" приведет к корректному рендеру на всем железе.
  • 4. Приложение берет на себя ответственность сообщать WDDM, какие ресурсы (а точнее какие регионы памяти, об этом ниже) используются командами при передаче набора команд GPU scheduler.
После этого легко заметить что драйвер больше фактически никакого стейта в ресурсах держать не должен,
потому что приложение решит все сложные вопросы.
Поэтому с точки зрения API ресурс теперь это такая небольшая структурка (типа 128 байт),
которая не приводит к созданию объектов в драйвере, там просто данные о ресурсе - что за ресурс, какого размера, где в памяти находятся данные, etc.
Это называется resource descriptor в обоих API.
Чтобы "устанавливать" ресурсы, используется понятие descriptor sets (Mantle)/descriptor tables (DX12)
Descriptor set (Mantle) - это массив дескрипторов ресурсов, в котором некоторые entries ссылаются на другие descriptor sets - т.е. такая иерархическая структура.
Например, у вас есть два массива, один для глобальных текстур (GT), второй для текстур материала (MT),
а при draw call вы ставите массив размера 4 который выглядит так [ref to GT, ref to MT, VB, IB]
Эти descriptor sets находятся в памяти которую умеет читать GPU.
Соответственно вам надо их аллоцировать, гарантировать что вы их освобождаете только когда GPU закончила с ними работу, итп.
Т.е. их нужно менеджить, раньше это делал драйвер - ваше приложение может это делать лучше.
Descriptor table (DX12) - это просто массив дескрипторов ресурсов без иерархии.
Зато у вас не один descriptor table описывающий все привязки ресурсов а несколько.
Т.е. глубокую иерархию делать нельзя, но можно поставить несколько штук.
Таблицы дескрипторов лежат в памяти которую вам предоставляет драйвер, из нее например может уметь читать GPU - комментарии про синхронизацию остаются в силе.
В-третьих, разберемся с механизмом генерации команд.
В DX10 ваше приложение генерирует вызовы API,
они складываются в некий буфер который отдается драйверу.
Драйвер его в отдельном треде обрабатывает и генерирует GPU command buffer.
(обычно с участием kernel-mode driver)
И потом отдает этот GPU command buffer в WDDM,
вместе со списком ресурсов которые надо актуализировать (иметь в памяти)
На кадр бывает больше чем 1 command buffer, зависит от драйвера.
Но чаще около одного.
Denis Mentey:
bindless общей табличкой получается сам выходит?
Arseny Kapoulkine:
Да, так точно. Можно сделать огромную табличку на миллион дескрипторов, и в шейдере выбирать по индексу - адресация таблички может быть динамической.
Arseny Kapoulkine:
В Mantle и DX12 все немного иначе.
В Mantle есть понятие queue - это очередь в которую можно класть command buffer-ы.
Этих queue обычно несколько - например, universal (graphics&compute) queue, compute queue, DMA queue.
Они соответствуют command processor-ам на GPU - т.е. например если GPU умеет одновременно запускать graphics work и compute work из разных потоков команд, то их несколько.
Чтобы выполнять команды в этих очередях, нужно создавать command buffer-ы и заполнять их командами.
Command buffer это ресурс в памяти, который доступен на GPU и содержит команды которые умеет читать GPU.
Один и тот же command buffer можно записать один раз и отдавать на GPU много раз.
Чтобы работать с WDDM, при сабмите command buffer в queue нужно сообщить набор (массив) всех объектов памяти (см. ниже) которые должны быть в видеопамяти при выполнении.
Рекомендуется генерировать меньше сотни command buffers на кадр, потому что есть ненулевая стоимость запуска с т.з. WDDM.
Кроме command buffers в queues можно вставлять например синхронизационные примитивы - fences.
Чтобы уметь освобождать ресурсы после того как они уже не нужны на GPU итп.
В DX12 есть понятие command queue - это очередь в которую можно класть command buffer-ы (ничего, что я повторяюсь?..)
В DX12 они называются command lists.
Отличие от Mantle в том, что command list нельзя запускать несколько раз если я правильно понимаю.
Отличие от DX11 command lists в том что они полностью self contained - они не наследуют никакого стейта "снаружи" (собственно, понятие стейта снаружи отсутствует - единственное что можно делать в command queue - это самбитить command lists и синхронизационные примитивы)
Чтобы уметь кешировать команды, создано понятие bundles.
Bundle это набор команд который создан заранее и может применяться несколько раз.
Есть ограничения на типы команд которые могут быть в bundles (например, RT setup вроде как быть не может)
Bundles наследуют "снаружи" привязку ресурсов и чуть-чуть стейта.
Bundles можно выполнять несколько раз в контексте command buffer (при этом команды из bundles видимо копируются в command buffer)
При генерации или запуске command buffer (не помню, когда точно) надо сообщить WDDM, какие объекты памяти (см. ниже) должны быть в видеопамяти при выполнении.
И значит вот и в Mantle и в DX12 прорыв - эти самые command buffers/lists можно генерировать в любых потоках!
Т.е. натурально вы запускаете код в другом потоке,
он в том же потоке вызывает драйвер,
драйвер в том же потоке записывает команды в память.
Никаких driver threads не надо, работы от kernel-mode driver почти не требуется.
Все счастливы.
В-четвертых, разберемся с памятью.
Я тут уже раз 5 говорил что память надо менеджить средствами WDDM - у WDDM надо запрашивать блоки памяти, и при отрисовке говорить WDDM какие блоки нужно актуализировать.
Поэтому в Mantle введено понятие memory object еяпп (в DX12 называется memory heap) -
это кусок памяти который аллоцируется и про который вы должны рассказать WDDM что его надо держать в памяти,
а внутри этого куска памяти вы можете размещать данные ресурсов так как считаете нужным.
Mantle умеет вам рассказывать для каждого ресурса, какие у него требования про alignment итп (типа, текстура 32х32 ARGB8 должна быть выравнена на 32 байта в пределах куска памяти; рендер таргет 54x1 RGBA16F должен быть выравнен на 1024 байта в пределах куска памяти)
В DX12 вроде бы эти требования стандартизованы (я люблю MS)
Соответственно, вы можете делать разное.
Можно аллоцировать каждый буфер в свежем memory object/heap -
это неэффективно.
Можно сделать аллокатор который создает большие heaps и в них внутри использует какой-нибудь dlmalloc.
Можно сказать что вы обладаете для ряда ресурсом знанием свыше -
например, уровень грузится из пака как единое целое,
и поэтому на уровень достаточно проаллоцировать свежий heap,
и так далее.
В Mantle тоже есть понятие memory heap, я на нем останавливаться не буду - оно не очень критичное, но означает другое.
Ну и в-пятых!
Mantle вроде как работает на всем GCN железе и будет работать на будущем железе AMD.
Public релиз намечен на этот год (2014)
Сейчас чтобы получить доступ к API надо подписать NDA с AMD (точнее, это можно будет сделать в апреле)
DX12 будет работать на всем DX11 железе от NVidia, AMD; Intel обещала что карточки из Haswell будут работать.
Это больше половины всей steam аудитории на момент лонча.
Ближе к концу этого года будет community technology preview, финальный релиз с драйверами от всех вендоров итп будет в следующем году - если вы делаете игру которая шипится в holiday season 2015 то кажется можно использовать DX12.
Еще DX12 будет работать на windows phones, так что если вы вдруг делаете игры под windows phone (зачем???) то счастье близко.
А еще DX12 будет работать на XBox One.
С т.з. Mantle драйвера уже сейчас есть от AMD и на них уже можно поиграть в BF4.
С т.з. DX12 у NVidia уже есть драйвера на которых показывали всякие демки.
AMD дописывает первую версию своего DX12 драйвера.
Arseny Kapoulkine:
Напоследок, в DX12 обещают новые GPU фичи - видимо, под feature levels.
Конкретно я записал про Интеловский pixelsync везде.
И про hardware JPEG decoding.
И - поскольку спрашивали - Mantle использует для шейдеров DX11 shader bytecode, есть транслятор в AMD IL в составе Mantle SDK.
Arseny Kapoulkine:
Информация, доступная on-line:
http://blogs.msdn.com/b/directx/archive/2014/03/20/directx-12.aspx - блог пост вкратце про DX12.
http://www.slideshare.net/DICEStudio/rendering-battlefield-4-with-mantle - слайды доклада на который я не пошел про BF4 и Mantle.
http://blogs.nvidia.com/blog/2014/03/20/directx-12/ - блог пост от NVidia про поддержку железом итп.
Подведем итог.
Как сказал Семен, миссия Mantle выполнена - будет DX12.
Mantle в некоторых участках чуть более low level (типа, можно сохранять pipeline state objects, etc.) - но во-первых DX12 еще не финализирован, а во-вторых кому нужно AMD-only API на PC - непонятно.
С другой стороны, DX12 - это весь high-end PC плюс XBoxOne.
Адаптировать рендер под DX12 - это очевидно ненулевой объем работы, если вы писали графику под консоли - будет проще. Но собственно параллельный диспатч команд плюс действительно дешевые draw calls для больших игр того стоят.
Главный вопрос - будет ли DX12 под Windows 7.
Если нет - придется ждать OpenGL 5, ыыы.
Simon Kozlov:
Возникла у меня грустная мысль, что AMD не закрыла Mantle в свете DX12 только потому что не будет его на Win7...
Denis Mentey:
если в dx12 вплоть до названий многое совпадает и будет работать и на нвидии тоже.
Arseny Kapoulkine:
Я про такое думал, да.
Что единственный вариант при котором Mantle выживает - это если DX12 будет только Win8.1 или Win9.
Simon Kozlov:
И да, спасибо за рассказ! Я попробую отформатировать в формате хорошего блог-поста.