
3月上旬、レドモンドにあるマイクロソフトの本社のDirect3D開発チームを訪問することができました。 3Dアプリケーションのデバッグに関する議論の中で、シェーダーのデバッグに新しいDirectX10 \ 11機能を使用するように勧められました。
この手法を使用してDirectX 11のテセレーションコードをデバッグしました(このコードは以下に示します)が、DirectX 10にも同じ機能があり、移植は非常に簡単です。
私たちは何をしようとしていますか?
CPUを使用したこのデータの後続処理のために、GPUシェーダー(頂点、幾何学的、テッセレーション)で実行された作業の結果を取得することに関心があります。 同時に、画面上のグラフィックスの計算結果を確認し、すべての座標をRAM内のバッファーと構造の形式で保持します。これらの座標から、既に読み取り、ログへの書き込みを行い、さらに計算に使用します。
ビジネスに取り掛かろう
4つの基本的な手順を完了する必要があります。
シェーダーを変更する
受信するシェーダー出力にフィールドを追加する必要があります。 たとえば、通常の状態では、シェーダーはワールド空間座標を表示しない場合がありますが、Stream Outステージを介した出力をデバッグするために追加できます。
ジオメトリシェーダーの作成方法を変更する
ID3D11GeometryShader(またはID3D10GeometryShader)の構築とパイプラインへの追加は、異なる方法で行われます。
出力を受け取るためのバッファーを作成します
論理的には十分です-結果をどこかに保存する必要もあります。
結果の解読
バッファー内の受信データは、構造の配列です。各構造には、シェーダーで定義された形式の頂点に関する情報が含まれています。 バッファをデコードする最も簡単な方法は、同じ形式で構造体を宣言し、バッファの先頭へのポインタを上記の構造体の配列へのポインタにキャストすることです。
そのため、シェーダーを変更します
ご存知かもしれませんが、Direct3Dは「パスフォワード」メカニズムをサポートしています。 これは、パイプラインの前のステージの出力の結果が次のステージに渡されることを意味します(どのような方法でも戻りません)。 したがって、頂点シェーダーから追加のデータを取得する場合は、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構造に注意してください-将来、この構造のどの要素を受け取るかを選択します。
ピクセルシェーダーは変更する必要がないことに注意してください。 上記の例では、ピクセルシェーダーは構造体の2番目のパラメーター-float3 color:COLORのみを受け取り、他のすべてのパラメーターを無視します。 したがって、最も単純なアイデアを使用します。StreamOutステージに出力する新しいフィールドはすべて、DS_OUTPUT構造体の最後に追加されるだけです。
次に、ジオメトリシェーダーを作成する手順を変更します。 CreateGeometryShader()の代わりにCreateGeometryShaderWithStreamOutput()メソッドを呼び出して、構造D3D11_SO_DECLARATION_ENTRY(または、使用しているDirectXのバージョンに応じてD3D10_SO_DECLARATION_ENTRY)を渡す必要があります。
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);
次の3つの点に注意してください。
- セマンティック名:シェーダーのHLSLコードで記述されるように対応する必要があります。 上記の構造では、ジオメトリシェーダーで宣言された4つのフィールドから3つのフィールドを選択していることに注意してください。
- 初期要素と要素数:データ型float3の場合、それぞれゼロから始まる3つの座標すべてを取得したい場合、初期要素は0、数は3です。
- 2つの隣接する頂点間のステップ(オフセット):CreateGeometryShaderWithStreamOutput()を呼び出すには、頂点を記述する構造体のサイズを知る必要があります。 計算はそれほど難しくありませんが、間違えてsoDecl構造体のサイズを渡すことができますが、これは正しくありません。
次に、結果を取得するためにバッファを作成する必要があります。 頂点バッファーとインデックスバッファーを作成するのとほぼ同じ方法で作成されます。 2つのバッファが必要です。1つはGPUから読み取り可能、もう1つは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; }
D3D11_USAGE_DEFAULTフラグを使用して作成されたバッファーでMap()メソッドを呼び出すことはできません。また、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 ) );
制限はありますか?
残念ながら、はい。
- この全体の生産性はそれほど高くありません。 それでも、ビデオメモリからRAMにデータをコピーする必要がありますが、これはそれほど高速ではありません。 ただし、これはすべてデバッグメカニズムであり、ほとんどの場合、この手法は製品コードでは使用されないことに注意してください。
- このトリックは、ピクセルシェーダーでは機能しません。 パイプラインのピクセルシェーダーは既にStream Outステージの後にあります。
- この手法では、シェーダーを変更する必要があります-つまり プロジェクトのコードベース。 デバッグビルドとリリースビルドで異なるシェーダーを使用するか、リリースでパフォーマンスの低下に耐える必要があります。
- メインパイプラインに接続されているため、必要な情報を頻繁に取得することも、すべてのフレームを描画するよりも少なくすることもできません。
- 頂点形式を記述するデータ構造の全体サイズにはいくつかの制限があります。DirectX10の場合、これらは64スカラー値または2 KBのベクタータイプデータです。