[Перевод] Использование Stream Out-стадии для отдалки шейдеров в DirectX 10\11
В начале марта я имел удовольствие посетить команду разработки Direct3D в главном офисе Microsoft в Редмонде. По ходу одной из дискуссий об отладке 3D приложений они посоветовали мне использовать новую возможность DirectX10\11 для отладки шейдеров.Я использовал эту технику для отладки кода тесселяции под DirectX 11 (этот код приведён ниже), но и DirectX 10 обладает теми же возможностями и портирование будет достаточно тривиальным.
Что мы вообще пытаемся сделать? Нам интересно получить результаты работы выполняемых на GPU шейдеров (вершинных, геометрических, тесселяции) для последующей обработки этих данных с помощью CPU. При этом мы хотим и видеть результаты просчёта графики на экране, и иметь все координаты в виде буферов и структур в оперативной памяти, откуда мы их уже сможем прочитать, записать в лог, использовать для дальнейших расчётов. Давайте уже к делу Вам нужно выполнить 4 базовых шага: Модифицировать ваши шейдерыНужно добавить к выводу шейдера дополнительные поля, которые мы хотим получить. К примеру, в обычном состоянии ваш шейдер может не выводить world-space координаты, но для отладочного вывода через Stream Out-стадию вы можете их добавить.
Изменить способ создания геометрического шейдераКонструирование ID3D11GeometryShader (или ID3D10GeometryShader) и добавление его в пайплайн будет происходить иначе.
Создать буфер для получения выходных данныхДостаточно логично — вам же нужно где-то хранить полученные результаты.
Расшифровать результатыПолученные данные в буфере представляют собой массив структур, каждая из которых содержит информацию о вершине в определённом шейдером формате. Самый простой способ декодировать буфер — объявить структуру в том же формате, а затем привести указатель на начало буффера к указателю на массив вышеуказанных структур.
Итак, модифицируем шейдеры
Как вы, возможно, знаете, Direct3D поддерживает механизм «pass forward». Это означает, что результаты вывода предыдущей стадии пайплайна передаются следующей стадии (и уже никак не возвращаются назад). Таким образом, если вы хотите вывести какие-то дополнительные данные из вершинного шейдера — вам придётся «протянуть» их через HS/DS/GS стадии пайплайна.
Давайте посмотрим на вот такой геометрический шейдер:
struct DS_OUTPUT { float4 position: SV_Position; float3 colour: COLOUR; float3 uvw: DOMAIN_SHADER_LOCATION; float3 wPos: WORLD_POSITION; };
[maxvertexcount (3)]
void gsMain (triangle DS_OUTPUT input[3], inout TriangleStream
Нужно отметить, что ваши пиксельные шейдеры не требуют изменений. В примере выше пиксельный шейдер будет получать только второй параметр структуры — float3 colour: COLOUR и игнорировать все остальные параметры. Таким образом мы будем использовать простейшую идею: все новые поля, которые мы хотим вывести на Stream Out-стадии будут просто добавляться в конец структуры DS_OUTPUT.
Теперь модифицируем процедуру создания геометрического шейдера. Нужно вызвать метод CreateGeometryShaderWithStreamOutput () вместо CreateGeometryShader (), передав ему кроме шейдера структуру D3D11_SO_DECLARATION_ENTRY (или D3D10_SO_DECLARATION_ENTRY — смотря какую версию DirectX вы используете), описывающую формат вершин.
D3D11_SO_DECLARATION_ENTRY soDecl[] = { { 0, «COLOUR», 0, 0, 3, 0 } , { 0, «DOMAIN_SHADER_LOCATION», 0, 0, 3, 0 } , { 0, «WORLD_POSITION», 0, 0, 3, 0 } };
UINT stride = 9 * sizeof (float); // *NOT* sizeof the above array! UINT elems = sizeof (soDecl) / sizeof (D3D11_SO_DECLARATION_ENTRY);
Нужно обратить внимание на три вещи:
Семантические имена: они должно соответствовать записать в HLSL-коде вашего шейдера. Обратите внимание — в структуре выше мы выбираем три поля из объявленных в геометрическом шейдере четырёх. Начальный элемент и количество элементов: для типа данных float3 мы хотим получить все три координаты, начиная с нулевой, соответственно начальный элемент — 0, количество — 3. Шаг (смещение) между двумя соседними вершинами: вызов CreateGeometryShaderWithStreamOutput () требует знания размера структуры, описывающей вершину. Посчитать не так уж сложно, но можно ошибиться и передать размер структуры soDecl, что будет неверно. Теперь нужно создать буфер для получения результатов. Он создаётся примерно так же, как вы создаёте вершинные и индексные буферы. Нам нужно два буфера — один доступный для записи с GPU, второй — доступный для чтения с CPU.
D3D11_BUFFER_DESC soDesc;
soDesc.BindFlags = D3D11_BIND_STREAM_OUTPUT; soDesc.ByteWidth = 10×1024 * 1024; // 10mb soDesc.CPUAccessFlags = 0; soDesc.Usage = D3D11_USAGE_DEFAULT; soDesc.MiscFlags = 0; soDesc.StructureByteStride = 0;
if (FAILED (hr = g_pd3dDevice→CreateBuffer (&soDesc, NULL, &g_pStreamOutBuffer))) { /* handle the error here */
return hr; }
// Simply re-use the above struct
soDesc.BindFlags = 0; soDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; soDesc.Usage = D3D11_USAGE_STAGING;
if (FAILED (hr = g_pd3dDevice→CreateBuffer (&soDesc, NULL, &g_pStagingStreamOutBuffer))) { /* handle the error here */
return hr; } Вы не можете вызвать метод Map () на буфере, созданном с флагом D3D11_USAGE_DEFAULT и вы не можете привязать буфер с флагом D3D11_CPU_ACCESS_READ к Stream Out-стадии пайплайна, так что вы создаёте по одному буферу каждого типа и копируете данные из одного в другой.
Теперь привязываем буфер к Stream Out-стадии:
UINT offset = 0; g_pContext→SOSetTargets (1, &g_pStreamOutBuffer, &offset);
Ну и давайте наконец прочитаем результаты из буфера:
g_pContext→CopyResource (g_pStagingStreamOutBuffer, g_pStreamOutBuffer);
D3D11_MAPPED_SUBRESOURCE data; if (SUCCEEDED (g_pContext→Map (g_pStagingStreamOutBuffer, 0, D3D11_MAP_READ, 0, &data))) { struct GS_OUTPUT { D3DXVECTOR3 COLOUR; D3DXVECTOR3 DOMAIN_SHADER_LOCATION; D3DXVECTOR3 WORLD_POSITION; };
GS_OUTPUT *pRaw = reinterpret_cast< GS_OUTPUT* >(data.pData);
/* Work with the pRaw[] array here */ // Consider StringCchPrintf () and OutputDebugString () as simple ways of printing the above struct, or use the debugger and step through. g_pContext→Unmap (g_pStagingStreamOutBuffer, 0); } Всё вышеуказанное нужно выполнять после вызова рисования. Нужно быть внимательными со структурой, к указателю на которую вы преобразовываете содержимое буфера (учитывать выравнивание).
Сколько данных получено? Мы можем написать код с использованием запроса D3D11_QUERY_PIPELINE_STATISTICS для того, чтобы это выяснить.
// When initializing/loading D3D11_QUERY_DESC queryDesc; queryDesc.Query = D3D11_QUERY_PIPELINE_STATISTICS; queryDesc.MiscFlags = 0; if (FAILED (hr = g_pd3dDevice→CreateQuery (&queryDesc, &g_pDeviceStats))) { return hr; } // When rendering g_pContext→Begin (g_pDeviceStats);
g_pContext→DrawIndexed (3, 0, 0); // one triangle only
g_pContext→End (g_pDeviceStats);
D3D11_QUERY_DATA_PIPELINE_STATISTICS stats; while (S_OK!= g_pContext→GetData (g_pDeviceStats, &stats, g_pDeviceStats→GetDataSize (), 0)); Какие-либо ограничения? К сожалению, да.
Производительность всего этого дела не очень высока. Всё-таки нам приходится копировать данные из видеопамяти в оперативную память, что не очень быстро. Нужно, однако, помнить, что всё это является отладночным механизмом и в продакшн-коде эта техника использоваться, скорее всего, не будет. Этот трюк не работает для пиксельных шейдеров. Пиксельный шейдер в пайплайне находится уже после Stream Out-стадии. Эта техника требует изменения шейдеров — т.е. кодовой базы вашего проекта. Вам придётся либо использовать разные шейдеры в дебаг и релиз-билдах, либо смириться с некоторым падением производительности в релизе. Мы привязаны к основному пайплайну — мы не можем получить нужную нам информацию ни чаще, ни реже чем рисуется каждый кадр. Есть некоторые ограничения на общий размер структуры данных, описывающей формат вершины — для DirectX10 это 64 скалярных значения или 2 Кб данных векторного типа.