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

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

< Лекция 4 || Лекция 5: 123456789101112
5.3.2. Команды

Получив представление о регистрах, давайте познакомимся с форматом команд языка Vertex Shader 1.1. Если вы уже сталкивались с программированием процессоров архитектуры x86, то заметите некоторое сходство между ассемблерными командами языка Vertex Shader и командами процессоров x86. Все команды вершинного процессора имеют следующий синтаксис:

op dst, src0 [, src1] [, src2]

где

  • op - идентификатор команды.
  • dst - регистр назначения, в который записываются результаты команды.
  • src0, src1, src2 - регистры-операнды с исходными данными. Количество операндов варьируется от команды к команде.

Важной особенностью языка Vertex Shader 1.1 является жесткое ограничение на размер вершинного шейдера: число ассемблерных команд не может превышать 128. Это не так уж и много, поэтому разработчикам нередко приходится бороться буквально за каждую команду, чтобы втиснуть алгоритм в прокрустово ложе вершинного процессора.

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

MOV - Пересылка данных

Начнем с команды пересылки из регистра в регистр, синтаксис которой очень сильно напоминает аналогичную команду процессоров семейства x86:

mov dst, src

где

  • dst - регистр приемник;
  • src - регистр источник.

Следующая команда копирует содержимое константного регистра c2 во временный регистр r5:

mov r5, c2

Язык Vertex Shader позволяет обращаться к отдельным компонентам векторного регистра: для этого после названия регистра необходимо поставить точку ". " и перечислить названия компонентов, к которым вы собираетесь обратиться. При этом допускается переставлять компоненты местами и многократно дублировать один и тот же компонент вектора. В общем, синтаксис очень напоминает синтаксис языка HLSL для доступа к отдельным компонентам вектора. Так же имеется возможность изменить знак компонентов регистра перед передачей в команду. Например, следующая команда занесет в регистр r5 лишь первые три компонента регистра c2 с измененными знаками, при этом первые два компонента будут переставлены местами:

mov r5.xyz, -c2.yxz

ADD – Сложение

Команда add выполняет сложение двух регистров:

add dst, src0, src1

где

  • dst - регистр приемник, в который заносится результат.
  • src0 - регистр с первым слагаемым.
  • src1 - регистр со вторым слагаемым.

Действие команды можно описать выражением: dst = src0 + src1. Например, следующая команда выполняет сложение содержимого регистров v0 и c0 и заносит результат в регистр r0 add r0, v0, c0.

SUB – Вычитание

Данная команда выполняет вычитание двух регистров:

sub dst, src0, src1

где

° dst = src0 - src1

Примечание

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

К примеру, следующая команда вычитает из компонентов x и y регистра v2 компоненты z и w регистра v3 и заносит результат в компоненты y и z регистра r1:

sub r1.yz, v2.xy, v3.zw

MUL – Умножение

Перемножает два регистра:

mul dst, src0, src1

где

dst = src0 • src1

Следующая команда умножает все компоненты регистра c0 на компонент x регистра c1 и заносит результат в регистр r0:

mov r0, c0, c1.xxxx

MAD - умножение и сложение

Перемножает два регистра и прибавляет к полученному результату содержимое третьего регистра:

mad dst, src0, src1, src2

Где

dst = src0 • src1 + src2

Стоит отметить, что данная команда обычно выполняется значительно быстрее комбинации команд mul и add. Ниже приведен пример умножения регистра c0 на v1 с прибавлением к результату значения вектора c1. Результат заносится в регистр r0:

mad r0, c0, v1, c1

DP3 - скалярное произведение трехмерных векторов

Вычисляет скалярное произведение компонентов x, y, z двух векторов:

dp3 dst, src0, src1

Где

dst.xyzw = src0.x • src1.x + src0.y • src1.y + src0.z • src1.z

Например, следующая команда занесет во все компоненты регистра r1 результат скалярного произведения первых трех компонентов регистров v0 и r0:

dp3 r1, v0, r0

DP4 - скалярное произведение четырехмерных векторов

Вычисляет скалярное произведение содержимого двух регистров:

dp4 dst, src0, src1

где

dst.xyzw = src0.x • src1.x + src0.y • src1.y + src0.z
 • src1.z + src0.w • src1.w

Следующая команда занесет в первый компонент регистра r1 результат скалярного произведения всех четырех компонентов регистров v0 и r0:

dp4 r1.x, v0, r0

FRC - вычисление {x}

Возвращает дробную часть компонентов вектора:

frc dst, src0

где

dst = {src0}

Примечание

Обратите внимание, что {2.3}=3, но {- 2.3}=0.7

Результат может быть занесен только в компоненты y или xy регистра-приемника (запись в компонент x без y недопустима). Следующая команда заносит в компоненты xy регистра r0 дробные части соответствующих компонентов регистра r1: frc r0.xy, r1.

Команда frc в действительности является макрокомандой, которая разбивается на три команды. Принимая во внимание жесткие ограничения на длину вершинного шейдера, это весьма немаловажный нюанс. Многие вершинные процессоры поддерживают ее на аппаратном уровне, в результате чего при компиляции ассемблерного кода Vertex Shader 1.1 в микрокод вершинного процессора развернутый макрос frc может быть вновь заменен одной командой. Однако ограничение длины вершинного шейдера накладываются именно на число команд промежуточного кода Vertex Shader 1.1, поэтому даже если текущий вершинный процессор аппаратно поддерживает команду frc, при подсчете длины шейдера она все равно будет засчитана за три команды.

RCP - вычисление 1/x

Выполняет деление единицы на скалярный аргумент:

rcp dst, src0

где

dst.xyzw=1/src0

src должен быть скалярной величиной. Если src0 равен 0, в dst заносится максимальное значение с плавающей точкой, поддерживаемое данным GPU (обычно порядка 10^38 ).

Примечание

GPU NVIDIA начиная с NV3x поддерживают значения Floating-Point Specials: -Inf (минус бесконечность), +Inf (бесконечность со знаком плюс), NaN (результат не определен) и т.п. Соответственно, на NV3x и последующих процессорах результат 1.0/0.0 равен +Inf.

Следующая команда вычисляет 1/r1.w и заносит результат в r0.w:

rcp r0.w, r1.w

EXPP - вычисление 2x с точностью 2-3 знака после запятой

Возводит 2 в степень скалярного аргумента с точностью 2-3 знака после запятой:

expp dst, src0

где

  • dst - регистр приемник, в который заносится результат возведения в степень и побочные результаты. В компонент x заносится результат возведения в степень целочисленной части аргумента, в компонент y дробная часть аргумента, компоненту z присваивается результат возведения в степень, а компоненту w единица.
  • src0 - степень, в которую возводится 2. Должна быть скалярной величиной.

Алгоритм работы14В Vertex Shader 2.0 команда EXPP претерпела серьезные изменения :

dest.x = 2^floor(src0)\\
dest.y = src0 - floor(src0)\\
dest.z = 2^src\\
dest.w = 1

Примечание

Побочные результаты работы команды часто используются, например, для нахождения дробной части числа.

Значение компонента z вычисляется с точностью 10 бит (2-3 знака после запятой).

Следующая команда вычисляет 2^rl.w и заносит его в r0.z. Остальные компоненты регистра не изменяются благодаря использованию маски .z.

rcp r0.z, r1.w

EXP - вычисление 2x с точностью 6-7 знаков после запятой

Возводит 2 в степень скалярного аргумента с точностью 21 бит (6-7 знаков после запятой):

expp dst, src0

где

dst.xyzw = 2src0

Данная команда в действительности является макрокомандой, транслируемой в 10 инструкций. Поэтому перед ее использованием следует хорошенько подумать, а действительно ли вам так сильно необходима большая точность, чем у команды expp.

Следующая команда вычисляет 2^r1x и заносит его в r0.y:

expp r0.y, r1.x

MIN - определение минимальных значений компонентов

Выполняет покомпонентное сравнение двух аргументов и возвращает компоненты, имеющие минимальное значение.

min dst, src0, src1

где

  • dst - регистр-приемник, в который заносятся компоненты с минимальными значениями.
  • src0 и src1 - вектора, компоненты которых сравниваются.

Алгоритм работы:

dst = src0;
if (src0.x > src1.x)
dst.x=src1.x; if
 (src0.y > src1.y)
dst.y=src1.y; if
(src0.z > src1.z)
dst.z=src1.z; if 
(src0.w > src1.w)
dst.w=src1.w;

Следующая команда сравнивает компоненты w регистров r4 и r0, и заносит результат в компоненты x и y регистра r3: min r3.xy, r4.w, r0

MAX - определение максимальных значений компонентов

Выполняет покомпонентное сравнение двух аргументов и возвращает компоненты, имеющие максимальное значение.

max dst, src0, src1

где

  • dst - регистр-приемник, в который заносятся компоненты с минимальными значениями.
  • src0 и src1 - вектора, компоненты которых сравниваются.

Алгоритм работы:

dst = src0;
if (src0.x > src1.x)
dst.x=src0.x; if
(src0.y > src1.y)
dst.y=src0.y;
if (src0.z > src1.z)
dst.z=src0.z; if 
(src0.w > src1.w)
dst.w=src0.w;

Следующая команда сравнивает все компоненты регистров r4 и r2, и заносит результат в регистр r1: max r1, r4, r2

SGE - сравнение "если больше или равно"

Покомпонентно сравнивает содержимое двух регистров и возвращает 1, если компонент первого аргумента больше второго или равен ему, и 0 в противном случае:

sge dst, src0, src1

где

  • dst - регистр приемник, в который заносится вектор с результатами сравнения.
  • src0 и src1 - вектора, компоненты которых требуется сравнить.

Алгоритм работы:

dst.xyzw = 0;
if (src0.x >= src1.x)
dst.x = 1; if (src0.y
 >= src1.y)
dst.y = 1; if (src0.z
 >= src1.z)
dst.z = 1; if (src0.w
 >= src1.w)
dst.w = 1;

SLT - сравнение "если меньше"

Покомпонентно сравнивает два регистра и возвращает 1, если компонент первого аргумента меньше второго, и 0 в противном случае:

slt dst, src0, src1

где

  • dst - регистр приемник, в который заносится вектор с результатами сравнения.
  • src0 и src1 - вектора, компоненты которых требуется сравнить.

Алгоритм работы:

dst.xyzw = 0;
if (src0.x< src1.x)
dst.x = 1; if (src0.y
 < src1.y)
dst.y = 1; if (src0.z
 < src1.z)
dst.z = 1; if (src0.w
 < src1.w)
dst.w = 1;

В принципе, знания вышеперечисленных команд вполне достаточно для чтения кода простых вершинных шейдеров. А при столкновении с незнакомыми командами вы всегда сможете найти их описание в документации DirectX.

Примечание

Чтобы быстро найти информацию о незнакомой команде языка Vertex Shader откройте документацию по "неуправляемому " DirectX (Start | All Programs | Microsoft DirectX SDK | DirectX Documentation | DirectX SDK Documentation for C++) и введите во вкладке Index название интересующей вас команды. А для быстрого доступа к описанию всех команд и регистров интересующей вас версии Vertex Shader наберите во вкладке Index нужный идентификатор: vs_1_1, vs_2_0, vs_2_x или vs_3_0.

5.3.3. Разбираем код простого шейдера

Ну что ж, настало время попрактиковаться в использовании полученных знаний на практике. В качестве упражнения мы проанализируем ассемблерный код нашего старого знакомого – эффекта вершинной закраски BlackAndWhite.fx. Чтобы облегчить поиск соответствий между вершинным шейдером на языке HLSL и его ассемблерным кодом, я еще раз приведу код эффекта и листинг ассемблерного кода вершинного шейдера, полученного посредством FX Composer 2.0. Итак, код эффекта:

struct VertexInput 
{
float3 pos : POSITION;
float4 color : COLOR; 
};
struct VertexOutput 
{
float4 pos : POSITION;
float4 color : COLOR; 
};
VertexOutput MainVS(VertexInput input) 
{
VertexOutput output;
output.pos = float4(input.pos, 1.0f);
float luminance = (input.color.r+input.color.g+input.color.b)/3.0; 
output.color.r = luminance;
 output.color.g = luminance; output.color.b = luminance; 
 output.color.a = input.color.a;
return output; 
}
float4 MainPS(float4 color:COLOR):COLOR 
{
return color; 
}
technique BlackAndWhiteFill 
{
pass p0
 {
VertexShader = compile vs_1_1 MainVS(); PixelShader = compile 
ps_1_1 MainPS(); 
} 
}

И ассемблерный код вершинного шейдера:

vs_1_1
def c0, 0.333333343, 1, 0, 0
dcl_position v0
dcl_color v1
add r0.w, v1.y, v1.x
add r0.w, r0.w, v1.z
mul oD0.xyz, r0.w, c0.x
mad oPos, v0.xyzx, c0.yyyz, c0.zzzy
mov oD0.w, v1.w

Первая директива ассемблерного кода задает версию языка Vertex Shader, на котором написан эффект. В настоящее время поддерживаются 4 директивы c интуитивно понятными названиями, каждая из которых соответствует определенной версии языка Vertex Shader: vs11, vs20, vs2x и vs30. Нетрудно догадаться, что ассемблерный код нашего шейдера написан на версии 1.1.

Ниже расположена директива def, которая заносит в константный регистр c0 вектор (0.333333343, 1, 0.0), компоненты которого будут использоваться инструкциями вершинного шейдера. Данная операция выполняется один раз перед началом визуализации с использованием шейдера и поэтому не влияет на производительность.

Следующие две директивы dclposition и dclcolor указывают, что координаты текущей вершины будут помещаться во входной регистры v0, а цвет вершины - в регистр v1.

Далее начинается собственно код вершинного шейдера. Первые две команды выполняют сложение трех цветовых компонентов цвета вершины и заносят результат в компонент w регистра r0. Третья команда умножает полученную сумму на содержимое компонента x регистра c0, равного 0.333333343, то есть фактически сумма делиться на 3. Итоговый результат заносится в компоненты x, y, z выходного регистра цвета oD0. Таким образом, первые три команды вершинного шейдера соответствуют следующему коду HLSL:

output.color.rgb = (input.color.r+input.color.g+input.color.b)/3.0;

Как видно, умный компилятор HLSL избавился от лишней временной переменной luminance, а так же заменил присвоение значений трем компонентам r, g, b одним скалярным присваиванием. Но заменить два сложения и унижение векторным произведением ему все же не хватило сообразительности.

Продолжим анализ кода вершинного шейдера. Следующая команда mad может ввести начинающего разработчика в замешательство. Откуда она взялась, ведь HLSL -код вершинного шейдера не содержит чего-либо подобного? И что же она выполняет? Давайте немного подумаем. Данная команда madd использует в качестве аргументов координаты вершины из регистра v0 и компоненты константного регистра c0, а результат заносится в выходной регистр oPos, соответствующий выходным координатам вершины. Попробуем подставить в выражение, вычисляемое командой mad значение компонентов константного регистра c0:

oPos = v0.xyzx * c0.yyyz + c0.zzzy = v0.xyzx * 
(1, 1, 1, 0) + (0, 0, 0, 1) = (v0.xyz, 0) + (0, 0, 0, 1)

Таким образом, команда mad соответствует нижеприведенной строке HLSL -кода:

output.pos = float4(input.pos, 1.0f);

Получается, компилятор нашел изящный способ реализации этого HLSL -кода: вместо прямолинейного кода из двух команд

mov oPos.xyz, v0.xyz
mov oPos.w, c0.y

компилятор обошелся единственной командой mad.

Наконец, последняя команда mov заносит в альфа-канал выходного цветового регистра oD0 значение альфа-канала цвета вершины, т.е. соответствует строке

output.color.a = input.color.a

Оптимизируем вершинный шейдер

Итого код шейдера насчитывает 5 инструкций, и как мы выяснили в разделе 5.2.4, его обработка на GPU NV4x и G7x обработка занимает 7 тактов. Настало время подумать, как можно улучшить производительность эффекта. Обратим внимание на два факта:

  1. Компилятор HLSL не смог заменить сложение компонентов цвета с последующим делением на 3 скалярным произведением. Значит, имеет смысл попробовать переписать код эффекта с использованием встроенной в HLSL функции dot.
  2. Наше приложение, использующее данный эффект, не активирует режим альфа-смешивания ( alpha blending ). Соответственно, оно абсолютно некритично к значению альфа-канала. Однако, как мы выяснили, присвоение значения альфа-каналу выливается в дополнительную команду. Поэтому данное присвоение можно безболезненно убрать, сократив код эффекта на одну команду.

Код эффекта, написанный с учетом вышеуказанных данных рекомендацией, находится в листинге 5.5:.

VertexOutput MainVS(VertexInput input) 
{
VertexOutput output;
output.pos = float4(input.pos, 1.0f); 
// Значение альфа-канала не используется приложением,
 поэтому нам все равно, что будет в него 
// занесено в компонент a
output.color.rgba = dot(input.color.rgba, 1.0/3.0);
return output; 
}
Листинг 5.5.

Ассемблерный данного эффекта, полученный посредством FX Composer 2.0, приведен ниже:

// Generated by Microsoft (R) D3DX9 Shader Compiler 9.12.589.0000
vs_1_1
def c0, 0.333333343, 1, 0, 0
dcl_position v0
dcl_color v1
dp4 oD0, v1, c0.x
mad oPos, v0.xyzx, c0.yyyz, c0.zzzy
// approximately 2 instruction slots used

Как видно, компилятор теперь использует инструкцию скалярного произведения dp4, а инструкция mov с копированием альфа-компонента цвета исчезла. Таким образом, анализ ассемблерного кода позволил нам внести в эффект небольшие косметические преобразования и сократить размер ассемблерного кода в 2.5 раза (с 5 до 2 команд). А выполнив повторный анализ производительности эффекта (нажав кнопку Run на панели Shader Perfomance ) мы увидим, что время обработки вершины одним вершинным процессором сократилось с 7 до 3-х тактов, то есть в 2.3 раза. И если раньше видеокарта GeForce 7800 GTX могла обработать за 1 секунду 491.000.000 вершит, то теперь теоретическая производительность шейдера достигла немыслимого темпа 1.146.000.000 вершин в секунду.

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

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