- PVSM.RU - https://www.pvsm.ru -

Использование Stream Out-стадии для отладки шейдеров в DirectX 10-11

Использование Stream Out стадии для отладки шейдеров в DirectX 10 11
В начале марта я имел удовольствие посетить команду разработки Direct3D в главном офисе Microsoft в Редмонде. По ходу одной из дискуссий об отладке 3D приложений они посоветовали мне использовать новую возможность DirectX1011 для отладки шейдеров.

Я использовал эту технику для отладки кода тесселяции под DirectX 11 (этот код приведён ниже), но и DirectX 10 обладает теми же возможностями и портирование будет достаточно тривиальным.

Что мы вообще пытаемся сделать?

Нам интересно получить результаты работы выполняемых на GPU шейдеров (вершинных, геометрических, тесселяции) для последующей обработки этих данных с помощью CPU. При этом мы хотим и видеть результаты просчёта графики на экране, и иметь все координаты в виде буферов и структур в оперативной памяти, откуда мы их уже сможем прочитать, записать в лог, использовать для дальнейших расчётов.

Давайте уже к делу

Вам нужно выполнить 4 базовых шага:

Модифицировать ваши шейдеры
Нужно добавить к выводу шейдера дополнительные поля, которые мы хотим получить. К примеру, в обычном состоянии ваш шейдер может не выводить world-space координаты, но для отладочного вывода через Stream Out-стадию вы можете их добавить.

Изменить способ создания геометрического шейдера
Конструирование ID3D11GeometryShader (или ID3D10GeometryShader) и добавление его в пайплайн будет происходить иначе.

Создать буфер для получения выходных данных
Достаточно логично — вам же нужно где-то хранить полученные результаты.

Расшифровать результаты
Полученные данные в буфере представляют собой массив структур, каждая из которых содержит информацию о вершине в определённом шейдером формате. Самый простой способ декодировать буфер — объявить структуру в том же формате, а затем привести указатель на начало буффера к указателю на массив вышеуказанных структур.

Итак, модифицируем шейдеры

Как вы, возможно, знаете, Direct3D поддерживает механизм «pass forward». Это означает, что результаты вывода предыдущей стадии пайплайна [1] передаются следующей стадии (и уже никак не возвращаются назад). Таким образом, если вы хотите вывести какие-то дополнительные данные из вершинного шейдера — вам придётся «протянуть» их через 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<DS_OUTPUT> TriangleOutputStream )
{
    TriangleOutputStream.Append( input[0] );
    TriangleOutputStream.Append( input[1] );
    TriangleOutputStream.Append( input[2] );
    TriangleOutputStream.RestartStrip();
}

Этот геометрический шейдер полностью «прозрачен» — просто перенаправляет вход на выход. Обратите внимание на структуру DS_OUTPUT — в дальнейшем мы выберем, какие элементы этой структуры мы хотим получить.

Нужно отметить, что ваши пиксельные шейдеры не требуют изменений. В примере выше пиксельный шейдер будет получать только второй параметр структуры — 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);

Нужно обратить внимание на три вещи:

  1. Семантические имена: они должно соответствовать записать в HLSL-коде вашего шейдера. Обратите внимание — в структуре выше мы выбираем три поля из объявленных в геометрическом шейдере четырёх.
  2. Начальный элемент и количество элементов: для типа данных float3 мы хотим получить все три координаты, начиная с нулевой, соответственно начальный элемент — 0, количество — 3.
  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 Кб данных векторного типа.

Автор: tangro

Источник [2]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/otladka/68417

Ссылки в тексте:

[1] пайплайна: http://msdn.microsoft.com/ru-ru/library/windows/desktop/ff476882(v=vs.85).aspx

[2] Источник: http://habrahabr.ru/post/234707/