среда, 18 мая 2011 г.

Packed Stream Output

Есть такой перекос в D3D 10/11 железе - input layout может фетчить данные из вершинного буфера в разнообразных форматах, а вот stream output может выводить только 32-битные значения (написано в са-а-амом конце раздела Getting Started with the Stream-Output Stage). Между тем, обычно float точность сильно избыточна для небольших моделей, и хотелось бы иметь возможность выводить данные типа half.

SM 5.0 предоставляет две функции для конвертирования float в half и обратно: f32tof16() и f16tof32(). Для них есть dedicated silicon в D3D11-железе и подразумевается, что они first-class citizen. Также эти функции могут применяться в SM 4.0, в этом случае они эмулируются программно - в шейдер вставляются многочисленные операции битового сдвига, целочисленного сложения и т. д. Я случайно нашёл С-аналоги этих функциий в OpenGL RedBook: Floating-Point Formats Used in OpenGL (вероятнее всего, алгоритм следует стандарту IEEE 754-2008 для half precision floating-point format). В своё время я написал подобные функции конвертирования, работающие в шейдере посредством lookup table, но в свете SM 5.0 это уже устарело :)

У меня появилась идея, что с помощью этих функций можно в геометрическом шейдере паковать два float-значения в один, ужимая каждый в half, и т. о. снизить нагрузку на stream output и на input assembler. Кроме того, можно уменьшить важный параметр maxvertexcount геометрического шейдера: например, если ранее GS выводил две вершины, то теперь достаточно одной. Общая идея такова: SO выводит по два half, запакованных во float, а IA интерпретирует вершинный буфер как R16G16B16A16_FLOAT, т. о. мы сможем легко читать каждую запакованную вершину.

Вот код упаковки двух трёхкомпонентных векторов в один четырёхкомпонентный: pck. Четвёртый компонент необходим вследствие того, что формат R16G16B16A16_FLOAT имеет четыре компоненты (6-байтные форматы с тремя компонентами не поддерживаются), но его можно игнорировать при чтении из вершинного буфера, а значит, и при упаковке.

Всё легко пакуется до тех пор, пока вы выводите чётное кол-во вершин: 2, 4 и т. д. А если нужно вывести 3 вершины (треугольник)? Можно запаковать первые две в один vec4, третью - в компоненты .xy второго vec4, компоненты .zw - оставить пустыми. Но при последующем чтении треугольника мы прочитам три half4, а пустой четвёртый будет относиться уже к следующему примитиву - ошибка! Указать же stride между примитивами в вершинном буфере невозможно - никому в голову не придёт идея оставлять в памяти ненужные "дырки".

Решение простое. Если раньше мы сжимали, скажем, четыре вершины в две, то теперь надо сжать три в одну:

struct gs_out
{
vec4 pos1_pos2 : Data0;
vec2 pos3 : Data1;
};

[maxvertexcount(1)]
void gs_main(..., inout PointStream< gs_out > stream)
{
...
}

В IA читаем данные из вершинного буфера как поледовательность half4 - и всё.

Комментариев нет:

Отправить комментарий