Опубликован: 28.04.2009 | Доступ: свободный | Студентов: 1840 / 107 | Оценка: 4.36 / 4.40 | Длительность: 16:40:00
Специальности: Программист
Лекция 5:

Вершинные шейдеры

< Лекция 4 || Лекция 5: 123456789101112

Преобразование кода эффекта тоже весьма тривиально (листинг 5.19).

public partial class MainForm : Form
{
// Файл эффекта для визуализации вращающегося диска
const string diskEffectFileName = "Data\\Disk.fx"; 
// Файл эффекта для визуализации разлетающихся искр
const string fireworkEffectFileName = "Data\\Firework.fx";
// Эффект визуализации диска
Effect diskEffect = null;
 // Объект, инкапсулирующий параметр angle эффекта диска
EffectParameter angleParam = null;
// Эффект визуализации искр
Effect fireworkEffect = null;
 // Объекты, инкапсулирующие параметры эффекта искр: 
 diskSpeed, time, timeLoopParam
EffectParameter diskSpeedParam = null;
EffectParameter timeParam = null;
EffectParameter timeLoopParam = null;
private void MainFormLoad(object sender, EventArgs e) 
{
// Так как вершины визуализируются без промежуточного 
 "эмулятора ", используется  "родная
 " // декларация формата вершин
fireworkDeclaration = new VertexDeclaration(device, 
 VertexPositionColorTexture.VertexElements);
try 
{ 
// Загружаем эффекты
diskEffect = Helper.LoadAndCompileEffect(device, 
diskEffectFileName);
fireworkEffect = Helper.LoadAndCompileEffect(device, 
fireworkEffectFileName); 
}
catch (Helper.LoadAndCompileEffectException ex)
{ 
// Обрабатываем исключительные ситуации загрузки и компиляции эффекта
closing = true;
MessageBox.Show(ex.Message, "Критическая ошибка", MessageBoxButtons.OK, 
MessageBoxIcon.Error);
Application.Idle += new EventHandler(ApplicationIdle);
return; }
// Получаем объект, инкапсулирующий параметр angle 
эффекта диска angleParam = diskEffect.Parameters["angle"];
 Debug.Assert(angleParam != null, diskEffectFileName + " : 
 не найден параметр angle");
// Получаем объект, инкапсулирующий параметр diskSpeed эффекта искр
 diskSpeedParam = fireworkEffect.Parameters["diskSpeed"];
Debug.Assert(diskSpeedParam != null, fireworkEffectFileName + " : 
не найден параметр diskSpeed");
// Получаем объект, инкапсулирующий параметр time эффекта искр
timeParam = fireworkEffect.Parameters["time"] ;
Debug.Assert(timeParam != null, fireworkEffectFileName + " : 
не найден параметр time") ;
// Получаем объект, инкапсулирующий параметр timeLoop эффекта искр
timeLoopParam = fireworkEffect.Parameters["timeLoop"];
Debug.Assert(timeLoopParam != null, fireworkEffectFileName + " :
 не найден параметр timeLoop"); 
}
private void MainFormPaint(object sender, PaintEventArgs e) 
{
float time = (float)stopwatch.ElapsedTicks / (float)Stopwatch.Frequency;
// Задаем значения параметров
timeParam. SetValue(time); 
timeLoopParam.SetValue(timeLoop); 
diskSpeedParam.SetValue(diskSpeed);
// Указывает декларацию формата вершин. Внимание! 
Если вы при переходе к другому формату 
// вершин и забудете подправить декларацию формата 
вершины, то часть входных параметров 
// вершины вроде текстурных координат будет содержать 
 "мусор ". Соответственно, эффект будет 
// работать весьма странно, а самом худшем случае это 
может привести к краху приложения и 
// даже операционной системы.
device.VertexDeclaration = fireworkDeclaration;
// Визуализируем искры как обычно
fireworkEffect.Begin();
for (int i = 0; i < fireworkEffect.
CurrentTechnique.Passes.Count; i++)
{
EffectPass currentPass = fireworkEffect.
CurrentTechnique.Passes[i] ; currentPass.Begin();
for (int j = 0; j < fireworkVertices.Length; j++) 
{
device.DrawUserPrimitives(PrimitiveType.PointList, 
fireworkVertices[j], 4> 0, fireworkVertices [j] .Length) ; 
}
currentPass.End(); 
} 
fireworkEffect.End();
// Выполняем приготовления к визуализации диска
device.RenderState.AlphaBlendEnable = false;
angleParam.SetValue(diskSpeed * time);
// Не забываем изменить декларацию формата вершины
device.VertexDeclaration = diskDeclaration;
// Визуализируем диск
...
// Вычисляем FPS
...
} 
}
Листинг 5.19.

Анализ исходного кода эффекта

Сейчас вы уже вполне неплохо освоились с языком Vertex Shader 1.1 , поэтому выполнять построчный анализ ассемблерного кода вряд ли имеет смысл. Вместо этого я сразу приведу отчет NVIDIA FX Composer 2.0 с ассемблерным листингом, разделенным комментариями на блоки, соответствующие тем или иным инструкциям.

//	Generated by Microsoft (R) D3DX9 Shader Compiler 9.12.589.0000
//
//	Parameters:
//
//	float diskSpeed;
//	float time;
//	float timeLoop;
//
//
//	Registers:
//
//	Name Reg Size
//		 	 ----
//	diskSpeed c0 1
//	time c1 1
//	timeLoop c2 1
//
//
//	Default values:
//
//	diskSpeed
//	c0 = { 0, 0, 0, 0 };
//
//	time
//	c1 = { 0, 0, 0, 0 };
//
//	timeLoop
//	c2 = { 0, 0, 0, 0 };
//
vs_1_1
def c3, 0.0416666418, -0.5, 1, 0 def c4, 4, 9.52380943,
0.104999997, 0.25 def c5, 0.5, 0.159154937, 0.25, -
0.00138883968
def c6, 6.28318548, -3.14159274, -2.52398507e-007, 
2.47609005e-005 dcl_position v0 dcl_color
 v1 dcl_texcoord v2 // float currentTime = time - input.pos.x;
1.	add r2.w, -v0.x, c1.x
// Начало вычисления float localTime = currentTime % timeLoop
mul	r0.w,	r2.w,	c2.x
add	r1.w,	c2.x,	c2.x
sge	r0.w,	r0.w,	-r0.w
mad	r0.w,	r0.w,	r1.w, -c2.x
rcp	r1.w,	r0.w
mul	r3.w,	r2.w,	r1.w
expp r4.y, r3.w
mov r1.w, r4.y
// Вычисление подвыражения input.texcoord / slowing из
// float2 t = min(localTime.xx, input.texcoord / slowing). 
Деление на константу заменено
// умножением
10.	mul r0.xy, v2, c4.yxzw
// Окончание вычисления float localTime = currentTime % timeLoop
11.	mul r3.w, r0.w, r1.w
// Окончание вычисления float2 t = min(localTime.xx, 
input.texcoord / slowing)
12.	min r0.xy, r0, r3.w
// float2 sCoord =	input.pos.yz + t * (input.texcoord - 
t * slowing / 2.0f)
mul r1.xy, r0,	c4.zwzw
mad r1.xy, r1,	-c5.x, v2
mad r0.xy, r0,	r1, v0.yzzw
// sCoord.y += diskSpeed * (time - localTime)
mad r3.w, r0.w, -r1.w, c1.x
mad r3.w, c0.x, r3.w, r0.y
// Начало вычисления output.pos.xy = sCoord.x * float2(sin(sCoord.y),
 cos(sCoord.y))
mad	r2.xy,	r3.w, c5.y, c5.zxzw
frc	r1.xy,	r2
mad	r1.xy,	r1, c6.x, c6.y
mul	r1.xy,	r1, r1
mad	r2.xy,	r1, c6.z, c6.w
mad	r2.xy,	r1, r2, c5.w
// Вычисление подвыражения (currentTime >= 0) оператора if
24.	sge r2.w, r2.w, c3.w
// Продолжение вычисления output.pos.xy = sCoord.x * 
float2(sin(sCoord.y), cos(sCoord.y))
25.	mad r2.xy, r1, r2, c3.x
// float remainTime = liveTime - localTime
26.	mad r1.w, r0.w, -r1.w, c4.x
// Продолжение вычисления output.pos.xy = sCoord.x * 
float2(sin(sCoord.y), cos(sCoord.y))
mad r2.xy, r1, r2, c3.y
mad r1.xy, r1, r2, c3.z
// Продолжение оператора if: вычисление подвыражения 
(remainTime > 0)
29.	slt r0.w, c3.w, r1.w
// output.color.a = remainTime / liveTime
30.	mul r1.w, r1.w, c4.w
// Продолжение оператора if: окончание вычисления значения условия 
// ((remainTime > 0) && (currentTime >= 0))
31.	mul r0.w, r2.w, r0.w
// Окончание вычисления выражения
// output.pos.xy = sCoord.x * float2(sin(sCoord.y), cos(sCoord.y))
// (умножение на sCoord.x)
32.	mul oPos.xy, r0.x, r1
// Окончание блока if. Если условное выражение блока 
if равно true, альфа компонент цвета 
// остается без изменений, иначе обнуляется.
33.	mul oD0.w, r1.w, r0.w
// output.pos.zw = float2(0.0, 1.0);
34.	mov oPos.zw, c3.xywz
// output.color.rgb = input.color.rgb
35.	mov oD0.xyz, v1
// approximately 37 instruction slots used

Пробежимся по наиболее интересным местам HLSL кода. Первым сюрпризом является трансляция вычисления выражения currentTime % timeLoop аж в целых 8 инструкций (с 2-й по 9-ю). Это обусловлено тем, что язык Vertex Shader 1.1 не содержит инструкции вычисления остатка отделения, соответственно компилятору приходится эмулировать ее посредством скудного набора инструкций. Ниже приведена реконструкция алгоритма нахождения остатка от деления на языке C#:

// Функция на языке C#, вычисляющая a%b. Написана 
приближенно к алгоритму, используемому в
// HLSL
static float mod(float a, float b)
{
// Если частное (a/b) является отрицательным числом, 
то изменяем знак у делителя (nb=-b)
float cmp;
if (a*b > -a*b) cmp 
= 1;
else
cmp = 0;
float nb = cmp * (b + b) - b;
// Вычисляем частное
float div = a * (1.0f / nb);
 // Находим дробную часть частного
float frac = div - (float)Math.Floor(div); 
// Вычисляем остаток
float result = frac * nb;
return result; 
}

Отдельно стоит отметить нахождение дробной части числа, до сих выполнявшаяся посредством макроса frc . Но при анализе кода нахождения остатка дизассемблер не смог распознать эту операцию, что дало нам возможность воочию увидеть, что в действительности скрывается за макросом frc. Думаю, вы ожидали увидеть здесь все что угодно, только не команду expp, вычисляющую приближенное значение 2^n. Правда компилятора интересует не само значение 2^n, а побочный результат команды, заносящей в компонент y вектора-результата дробную часть числа ( a – floor(a) ). В целом же из всего вышесказанного следует вывод, что, несмотря на обманчиво простой вид, оператор % языка HLSL является очень "дорогой " операцией, соизмеримой по времени выполнения с вычислением тригонометрических функций.

Чтобы максимально задействовать суперскалярную архитектуру современных вершинных процессоров, компилятор HLSL изменил их порядок следования, чтобы избавиться от зависимости соседних инструкций. Обратной стороной медали является сложность анализа кода: инструкции многих операторов HLSL перемешались между собой, а код строки float remainTime = liveTime – localTime, расположенной в начале эффекта, был перенесен компилятором ближе к концу шейдера.

Еще одной любопытной особенностью является код оператора if, составное условное выражение которого содержит логическую операцию "и " – так как язык Vertex Shader 1.1 не поддерживает булевские типы и логические операции над ними, оператор && эмулируется перемножением чисел с плавающей точкой.

Оптимизация вершинного шейдера

И, наконец, анализируя код строки float2 sCoord = input.pos.yz + t * (input.texcoord - t * slowing / 2.0f) мы обнаружим, что компилятор не смог догадаться предварительно вычислить значение константы slowing / 2.0f, что вылилось в один лишний оператор mul. Это дает нам основание предположить, что добавив в файл HLSL явное вычисление константы, мы сможем немного ускорить работу приложения. Но наверняка быть уверенным нельзя, ведь Vertex Shader 1.1 является всего лишь промежуточным кодом, впоследствии еще раз оптимизируемым компилятором драйвера.

Ну что ж, рискнем. Основные фрагменты эффекта с модифицированным вершинным шейдером приведены в листинге 5.20. Полный текст эффекта находится в example.zip в каталоге Examples\Ch05\Ex12.

static float2 slowing = {0.105, 0.25};
// Явно рассчитываем значение вспомогательной константы
static float2 slowing2 = slowing / 2.0f;
...
VertexOutput MainVS(VertexInput input)
{
...
float2 sCoord = input.pos.yz + t * (input.texcoord -t * slowing2); 
... 
}
Листинг 5.20.

Просмотр ассемблерного кода приложения даст вполне предсказуемые результаты: число команд ассемблерного листинга уменьшилось на одну (с 37 до 36), а вот время выполнения эффекта сократилось на целых 5 тактов (с 42 до 37) – вероятно удаление одной лишней команды позволило драйверу более эффективно распараллелить выполнение команд вершинного шейдера. В результате пиковая производительность эффекта на NVIDIA GeForce 7800 GTX увеличилась с 76.000.000 до 86.000.000 вершин в секунду, т.е. на 13%.

Таким образом, даже незначительные изменения в коде эффекта могут спровоцировать лавину изменений в финальном микрокоде шейдера для физического вершинного процессора, которые могут как усилить эффект от оптимизации HLSL -кода шейдера, так и свести ее на нет и даже снизить производительность.

5.5.4. Анализ производительности приложения

Настало время оценить эффект от переноса вычислений на видеокарту. На рисунке 5.23 приведена диаграмма, построенная в Excel по результатам измерения производительности примеров Ch05\Ex10 и Ch05\Ex12 на разных GPU.

 Производительность примеров Ch05\Ex10 и Ch05\Ex12 на разных GPU

увеличить изображение
Рис. 5.23. Производительность примеров Ch05\Ex10 и Ch05\Ex12 на разных GPU

Как видно, на GeForce 7600GT и Radeon X700 Pr o перенос вычислений с CPU на GPU увеличил частоту кадров почти в 10 раз. На компьютере с интегрированным GPU i946GZ (GMA 3000) частота кадров тоже заметно увеличилась (в 3.5 раза), что на первый взгляд выглядит весьма странно: i946GZ не содержит аппаратного вершинного процессора, поэтому все вычисления по-прежнему выполняются силами центрального процессора.

Данный парадокс обусловлен рядом факторов. Как известно, .NET приложения содержат множество вспомогательного кода для обнаружения различных внештатных ситуаций вроде переполнения или обращения к несуществующему элементу коллекции. Разумеется, этот код оказывает отрицательное влияние на производительность, усугубляемое многократным его выполнением в цикле. Кроме того, все современные процессоры еще со времен Pentium-III содержат специализированный векторные регистры SSE и набор векторных инструкций, отдаленно напоминающие ассемблерные команды языков Vertex Shader. Но язык C# и промежуточный язык IL не содержат векторных команд, что затрудняет распознавание векторных операций при компиляции JIT -компилятором IL -кода exe-файла в машинный код. В результате, итоговый машинный код практически не содержит SSE -инструкций и векторные блоки центрального процессора фактически простаивают.

При использовании вершинных шейдеров все обстоит несколько иначе. На i946GZ и аналогичных GPU без аппаратных вершинных процессоров вершинные шейдеры эмулируются DirectX посредством специальной подсистемы Processor Specific Geometry Pipeline (PSGP). PSGP автоматически выполняет компиляцию вершинного шейдера в набор инструкций текущего CPU, задействовав весь потенциал данного процессора на 100%. Полученный код активно использует блоки SSE, параллельную обработку нескольких вершин всеми ядрами CPU и не содержит каких-либо ненужных промежуточных проверок "на всякий случай ". В результате он работает заметно быстрее по сравнению с аналогом на C#, что мы и наблюдаем.

Итак, вершинные шейдеры позволяют значительно поднять производительность приложения. Но не стоит забывать, что это упреждение верно лишь при сравнении производительности C# и HLSL -кода, использующего одинаковый алгоритм. Центральный процессор предоставляет разработчику использовать значительно более гибкие алгоритмы, так что на практике все обстоит несколько сложнее. Но в любом случае, вершинные шейдеры позволяют разгрузить центральный процессор, освободив его ресурсы для других задач.

Заключение

В этой лекции мы познакомились с новыми возможностями языка HLSL применительно к программированию вершинных шейдеров: работе с отдельными компонентами вектора, математическими операторами, встроенными функциями, параметрами эффекта и особенностями оператора if. Так же была рассмотрена IDE для разработки шейдеров NVIDIA FX Composer 2.0, которая, учитывая рост сложности наших эффектов, пришлась как нельзя кстати. Учитывая, что вершинный шейдер выполняется для каждой вершины, число которых может измеряться сотнями тысяч, очень важно уделять внимание качеству кода и оптимизации вершинного шейдера. А для этого очень полезно иметь хотя бы поверхностное представление о том, что твориться под капотом HLSL, в частности о языках Vertex Shader. Поэтому мы изучили основы архитектуры виртуального процессора Vertex Shader 1.1 и его систему команд.

< Лекция 4 || Лекция 5: 123456789101112
Андрей Леонов
Андрей Леонов

Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету.