» 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

~5d ago

~8d ago

joreg: The Winter Season of vvvv workshops is now over but all recordings are still available for purchase: https://thenodeinstitute.org/ws23-vvvv-intermediates/

~14d ago

schlonzo: Love the new drag and drop functionality for links in latest previews!

~22d ago

joreg: Workshop on 29 02: Create Sequencers and Precise Clock Based Tools. Signup here: https://thenodeinstitute.org/courses/ws23-vvvv-08-create-sequencers-and-precise-clock-based-tools-in-vvvv-gamma/

~29d ago

joreg: Workshop on 22 02: Unlocking Shader Artistry: A Journey through ‘The Book of Shaders’ with FUSE. Signup here: https://thenodeinstitute.org/courses/ws23-vvvv-12-book-of-shaders/

~1mth ago

joreg: Talk and Workshop on February 15 & 16 in Frankfurt: https://visualprogramming.net/blog/vvvv-at-node-code-frankfurt/

~1mth ago

woei: @Joanie_AntiVJ: think so, looks doable

~1mth ago

xd_nitro: Anyone remember who increased projector brightness by removing some components that product the color?

~1mth ago

Joanie_AntiVJ: This looks super interesting (vectors over network) would anyone here know how to implement this in beta? https://github.com/madmappersoftware/Ponk