» DX11 Разбираемся с Constant Buffers
This site relies heavily on Javascript. You should enable it if you want the full experience. Learn more.

DX11 Разбираемся с Constant Buffers

English

Введение

Через constant buffers шейдеры обмениваются данными с внешним миром. Даже если мы не опишем их явно, они создадутся скрыто и будут делать свои «темные» дела в тайне от наших глаз).

Правильное использование constant buffers ведёт к существенному улучшению производительности. Поэтому читайте внимательно все, что тут написано.

Пример простого шейдера

float4x4 tW : WORLD;
float4x4 tVP : VIEWPROJECTION;
float4x4 tWVP : WORLDVIEWPROJECTION;
float4x4 tP : PROJECTION;
float Alpha <float uimin=0.0; float uimax=1.0;> = 1;
float4 cAmb <bool color=true;String uiname="Color";>;
float4x4 tTex <string uiname="Texture Transform"; >;
float4x4 tColor <string uiname="Color Transform";>;

Перед нами простой пример, в котором для экономии места мы объявили нужные переменные одну за другой.

Все переменные неявно и автоматически объединяются в один constant buffer. Если бы мы писали код с constant buffer'ом, то он выглядел бы так:

cbuffer cbGlobal : register( b0 )
{
    float4x4 tW : WORLD;
    float4x4 tVP : VIEWPROJECTION;
    float4x4 tWVP : WORLDVIEWPROJECTION;
    float4x4 tP : PROJECTION;
    float Alpha <float uimin=0.0; float uimax=1.0;> = 1;
    float4 cAmb <bool color=true;String uiname="Color";>;
    float4x4 tTex <string uiname="Texture Transform"; >;
    float4x4 tColor <string uiname="Color Transform";>;
};

Давайте разберемся с аббревиатурами:

  • tW : получает данные из пина эффекта "Transform In"
  • tVP and tP : получает данные из пинов Renderer'а
  • tWVP : автоматически объединяет tW и tVP
  • Когда мы вводим другие переменные, то добавляем пины ноде нашего эффекта

Подробнее о том, как передаются данные в шейдер можно, почитать в Dataflow in DX9

Обратите пристальное внимание вот на что:

  • tW, tVP, tTex, tP, tWVP в основном используются в вертексном шейдере
  • Остальные — в пиксельном шейдере

Обратите БОЛЕЕ пристальное внимание на вот это:

  • tVP and tP шейдер получает с ноды Renderer'а, и они остаются неизменными, вне зависимости от того, хоть один объект нам нужно отрисовать, хоть 512
  • Остальные переменные будут обновляться послайсово
  • Обратите внимание, что tWVP также обновляется послайсово, что приводит к перемножению матриц

Что же происходит по описанному сценарию:
На CPU создается структура, равная по размеру нашему буферу (с некоторыми отличиями)

Для каждого слайса:

  • Обновляется структура в зависимости от полученных на входе данных
  • Constant buffer выгружается на графическую карту
  • Constant buffer связывается с вертексным и пиксельным шейдерами
  • Происходит отрисовка
  • Все повторяется сначала

Теперь добавим немного веселой математики в нашу скучную статью. Внимательно посмотрите на строку:

  • Constant buffer выгружается на графическую карту

В нашем примере мы получили буфер следующего размера:
64(transform)* 6 + 16 (float4) + 4 (float) = 404 bytes

Теперь предположим, что нам нужно отрисовать 512 объектов, вместо одного, следовательно, на GPU мы должны выгрузить:
404*512 = 206848 bytes

Когда вычисления простые, как в нашем примере, размер буфера некритичен, но чем сложнее становятся ваши структуры, тем серьезнее нагрузка.

Поэтому оптимизируйте свои шейдеры.

Разделение буферов

Шейдеры позволяют использовать сразу несколько буферов, поэтому лучше наш буфер разбить на два:

cbuffer cbPerRender : register( b0 )
{
    float4x4 tVP : VIEWPROJECTION;
    float4x4 tP : PROJECTION;
};
 
cbuffer cbPerObject : register (b1)
{
    float4x4 tW : WORLD;
    float4x4 tWVP : WORLDVIEWPROJECTION;
    float Alpha <float uimin=0.0; float uimax=1.0;> = 1;
    float4 cAmb <bool color=true;String uiname="Color";>;
    float4x4 tTex <string uiname="Texture Transform"; >;
    float4x4 tColor <string uiname="Color Transform";>;
};

Теперь поведение шейдера изменится:

  • Все данные с Renderer'а соберутся в буфер cbPerRender
  • cbPerRender обновится только единожды
  • В следующих циклах будет обновляться только буфер b1

В результате мы сократили ресурсозатраты:
64 * 2 (мы тратим только один раз)
64 * 4 + 16 + 4 = 276 (тратим на каждый элемент)

При том же числе вызовов, на GPU мы передали 141440 байт, а это значительно меньше.

Обычно в процессе выполнения эффекта происходит автоматическое определение, что cbPerRender не задействован в работе пиксельного шейдера и они не связываются (а это позволяет сэкономить на вызовах API).

Упаковка буферов

Не знаю заметили вы или нет, но размеры constant buffer мы посчитали неверно ;)

GPU требует 16-байтовое выравнивание всех элементов constant buffer'а, а это значит, что все элементы выравниваются по значениям, кратным 16.

Проведем декомпозицию нашего кода, убрав аннотации для ясности

cbuffer cbPerObject : register (b1)
{
    float4x4 tW; //Offset = 0
    float4x4 tWVP; //Offset = 64
    float Alpha; //Offset = 128
    float4 cAmb; //Offset = 132
    float4x4 tTex; //Offset = 148
    float4x4 tColor; //Offset = 212
};
 
Общий размер : 276

В реальности размер должен получиться другим, так как оффсет 84, который мы указали для cAmb, не кратен 16. На самом деле наш cbuffer будет выглядеть по-другому:

cbuffer cbPerObject : register (b1)
{
    float4x4 tW; //Offset = 0
    float4x4 tWVP; //Offset = 64
    float Alpha; //Offset = 128
    float3 SomeRandomDataAdded ; //Offset = 132
    float4 cAmb; //Offset = 144
    float4x4 tTex; //Offset = 160
    float4x4 tColor; //Offset = 224
};
 
Общий размер : 288

Видели как cAmb сместился на 12 байт?

Произошло это по тем же причинам, что и в случае SIMD (описание которого выходит за рамки статьи): все элементы структуры должны укладываться в 16-байтовые границы.

Можно "упаковать" наш буфер иначе :

cbuffer cbPerObject : register (b1)
{
    float4x4 tW;
    float4x4 tWVP;
    float4 cAmb;
    float4x4 tTex;
    float4x4 tColor;
    float Alpha;
};

Размер его, конечно, будет таким же (ведь GPU все равно хочет, чтобы размер constant buffer'а был кратным 16), но теперь элементы буфера находятся на позициях, для которых не потребовалось специальное выравнивание.

Есть еще куча всего, что нужно учесть при работе с буферами, но эти моменты можно учитывать по ситуации (просто погуглите "packoffset hlsl").

To Spread or not to Spread?

Нода шейдера может принимать на вход спреды.

Если в пин приходит не спред, то эта часть буфера обновится только единожды

Допустим, cAmb и tColor не меняются (spread count всегда равен 1).

В этом случае буферы можно немного переписать:

cbuffer cbPerRender : register( b0 )
{
    float4x4 tVP : VIEWPROJECTION;
    float4x4 tP : PROJECTION;
    float4x4 tColor;
    float4 cAmb;
};
 
cbuffer cbPerObject : register (b1)
{
    float4x4 tW : WORLD;
    float4x4 tWVP : WORLDVIEWPROJECTION;    
    float4x4 tTex;
    float Alpha;
};

До тех пор, пока нода шейдера не начнет получать спреды на вход tColor или cAmb, буфер будет выгружаться единожды.

НО (ВНИМАНИЕ):
Если вы пошлете в пин cAmb спред, cbPerRender начнет обновляться в КАЖДОМ фрейме. И мы вернемся к тому, с чего начали, а это не есть хорошо.

Поэтому сразу определитесь для каких элементов буфера вы собираетесь использовать спреды на входе.

Этот момент станет еще важнее в последующих релизах нодов dx11.

Ещё немного мыслей

tWVP или mul(tW,tVP)

tWVP перемножает World, Texture и Projection на CPU, а функция mul (tW,tVP) работает уже на GPU.

А это две большие разницы: на CPU мы считаем tWVP для каждой модели, а mul(tW,tVP) производит расчеты для каждого вертекса. Поэтому их использование зависит от ситуации (заметьте, что pre transform не подходит для инстансинга, если только вы не собираетесь делать его на стадии сompute shader'а).

Лучше писать 2 версии шейдера и использовать тот, что быстрее (смысл запариваться есть только, если появляются какие-то проблемы с производительностью)

Моем кота и чистим код

Если мы не пользуемся color transform или чем-то подобным, то просто удаляем соответствующие строчки из кода, или пишем две версии шейдера: с ними и без.

Также избавляемся от ненужных операций пиксельного шейдера (например, делаем версии шейдера с текстурами и без).

anonymous user login

Shoutbox

~4h ago

joreg: vvvvTv S02E01 is out: Buttons & Sliders with Dear ImGui: https://www.youtube.com/live/PuuTilbqd9w

~7d ago

joreg: vvvvTv S02E00 is out: Sensors & Servos with Arduino: https://visualprogramming.net/blog/2024/vvvvtv-is-back-with-season-2/

~7d ago

fleg: hey there! What's the best tool for remote work? Teamviewer feels terrible. Thanks!

~21d ago

joreg: Last call: 6-session vvvv beginner course starting November 4: https://thenodeinstitute.org/courses/ws24-5-vvvv-beginners-part-i/

~1mth ago

joreg: Missed the last meetup? You can rewatch it here: https://www.youtube.com/live/MdvTa58uxB0?si=Fwi-9hHoCmo794Ag

~1mth ago

theurbankind: When is the next big event, like node festival ?

~1mth ago

~1mth ago

joreg: Join us for the next vvvv meetup on Oktober 17th: https://visualprogramming.net/blog/2024/25.-vvvv-worldwide-meetup/

~1mth ago

joreg: 6 session beginner course part 2 "Deep Dive" starts January 13th: https://thenodeinstitute.org/courses/ws24-5-vvvv-beginners-part-ii/