Через 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";>; };
Давайте разберемся с аббревиатурами:
Подробнее о том, как передаются данные в шейдер можно, почитать в Dataflow in DX9
Обратите пристальное внимание вот на что:
Обратите БОЛЕЕ пристальное внимание на вот это:
Что же происходит по описанному сценарию:
На CPU создается структура, равная по размеру нашему буферу (с некоторыми отличиями)
Для каждого слайса:
Теперь добавим немного веселой математики в нашу скучную статью. Внимательно посмотрите на строку:
В нашем примере мы получили буфер следующего размера:
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";>; };
Теперь поведение шейдера изменится:
В результате мы сократили ресурсозатраты:
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").
Нода шейдера может принимать на вход спреды.
Если в пин приходит не спред, то эта часть буфера обновится только единожды
Допустим, 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 перемножает World, Texture и Projection на CPU, а функция mul (tW,tVP) работает уже на GPU.
А это две большие разницы: на CPU мы считаем tWVP для каждой модели, а mul(tW,tVP) производит расчеты для каждого вертекса. Поэтому их использование зависит от ситуации (заметьте, что pre transform не подходит для инстансинга, если только вы не собираетесь делать его на стадии сompute shader'а).
Лучше писать 2 версии шейдера и использовать тот, что быстрее (смысл запариваться есть только, если появляются какие-то проблемы с производительностью)
Если мы не пользуемся color transform или чем-то подобным, то просто удаляем соответствующие строчки из кода, или пишем две версии шейдера: с ними и без.
Также избавляемся от ненужных операций пиксельного шейдера (например, делаем версии шейдера с текстурами и без).
anonymous user login
~4h ago
~7d ago
~7d ago
~7d ago
~21d ago
~1mth ago
~1mth ago
~1mth ago
~1mth ago
~1mth ago