Saturday, August 27, 2016

[DX12] HLSLにRoot Signatureを定義する

HLSLにRoot Signatureをattributeとして書く方法は、MSDNに詳しい説明があります。

Specifying Root Signatures in HLSL

MSDNのサンプルは機能紹介のためか「全部入り」の複雑なものになっていますが、ここではもっとシンプルなHLSLにRoot Signatureを書いてみました。キューブマップを空とみなして画面全体にレンダリングしています。


Vertex ShaderとPixel Shaderの両方に同じ定義をするために#defineで文字列を定義します。文字列1つで短く書けて、C++側で構造体と列挙体を駆使して書くより圧倒的に読みやすくで直感的です。

文字列の全体が、D3D12_ROOT_SIGNATURE_DESCに該当します。

StaticSamplerが、D3D12_STATIC_SAMPLER_DESCに該当する記述になります。フィルタリングの方法なども書けますが、レジスタ以外は省略可能です。嬉しいのはHLSL内でサンプラーの定義から使用まで完結することです。C++側から何もする必要がありません。

今回は書いていませんが、RootFlagsがD3D12_ROOT_SIGNATURE_FLAGSに該当します。ハマりそうな点としては、ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUTを書き忘れるとおそらく頂点入力が出来なくなるので気をつけます。今回は頂点はシェーダー内で生成しているので不要です。

RootFlagsとStaticSamplerを除いたそれ以外が、D3D12_ROOT_PARAMETERの配列と同等になります。この並びがC++側から参照するインデックス、つまりSetGraphicsRootConstantBufferViewやSetGraphicsRootDescriptorTableの引数RootParameterIndexに該当します。

こうしてHLSLのattributeに書いたRoot Signatureはシェーダのバイナリに含まれます。MSDNではfxcを使っていますが、D3DCompileFromFile等でID3DBlobを生成している場合にもD3DGetBlobPartでRoot Signatureを取り出せます。Vertex ShaderとPixel Shaderのどちらから取り出しても構いません。


D3DGetBlobPartにD3D_BLOB_ROOT_SIGNATUREを渡すことでシェーダーバイナリからroot signatureを取り出します。 これは、以前はD3D12SerializeRootSignatureで作っていたroot signatureのバイナリを置き換えるもので、どちらも最後はCreateRootSignatureでID3D12RootSignatureを生成します。

ところで、MSDNによるとDX11でもRoot Signature入りのシェーダを問題なく使えるようで、Root Signatureを単に無視すると書いてあります。別の視点から見ると、DX12でのみ必要だったコードをC++から追い出して、プラットフォームの差を吸収するのにも役立ってくれそうです。

Sunday, August 21, 2016

DX12でDDSファイルからテクスチャ生成

DirectX11でのDDSロードの記事はこちらです。

以前、GetCopyableFootprintsの記事では手抜きで生成していたテクスチャですが、今回はミップマップやキューブマップも考慮して真面目に生成してみます。

ところで、テクスチャのアップロードは既にMicrosoftのD3DX12.hのUpdateSubresources関数があります。D3DX12.hが優れている所としては、必要なリソースはUpdateSubresourcesの引数を通してのみ取得しており、透明であること、マルチスレッド化が容易であることが挙げられます。反面、やや使いにくい点としては、D3DX12.hはDX12特有の概念であるリソースバリアやアップロードバッファの管理を呼び出し側に委ねています。使用者はDX12のアーキテクチャを理解した上で使わなければいけないので、手軽に使えるものではないです。また、DX12を隠蔽するものではないので、マルチプラットフォーム化を目指した設計の中では使いにくそうです。

今回はD3DX12.hを使用しないで実装します。

DDSのロード


DDSファイルをメモリ上にまるごとロードして解析します。以前やったDX11版とほぼ同じです。

フォーマット、幅、高さ、配列数、ミップマップ、キューブマップかどうかの情報を取得してD3D12_RESOURCE_DESCを作り、CreateCommittedResourceを呼びます。また、サブリソース毎のピクセルデータの先頭位置とサイズをAFTexSubresourceData配列にまとめておきます。

テクスチャメモリへの転送


ここの処理の詳細は、以前やったGetCopyableFootprintsの記事を見てもらうと分かり易いと思います。違いは、今回の実装はミップマップやキューブマップもサポートします。

GetCopyableFootprintsでアップロードバッファの大きさと各サブリソースの配置方法を取得します。forループ内ではサブリソースをアップロードバッファに配置し、CopyTextureRegionで転送させます。リソースバリアはCopyTextureRegionのforループの前後で全てのサブリソースに対して一括で発行します。その為にD3D12_RESOURCE_BARRIERを配列で作っておきます。forループ内でサブリソース毎に個別にResourceBarrierを呼び出しても動きますが、マイクロソフトの開発者の動画 https://youtu.be/Db2TaG49SRgによるとまとめて一回呼ぶほうが良いとのことです。ここは、日本語で要約した記事も参考にしてください。

コマンドリスト実行前のID3D12Resourceの保護


最後に、アップロードバッファからテクスチャへの転送を行うコマンドリストの実行が終わるまで、転送元であるアップロードバッファと転送先であるテクスチャの双方が間違って解放されないように保護しておく必要があります。

AddIntermediateCommandlistDependentResourceがそれをやっていて、何をするかというとAddRefしてコンテナに追加しておくだけで、コマンドリストの終了を確認したら全てReleaseします。

アップロードバッファが保護されるべきであるのは分かり易い所ですが、「生成したばかりのテクスチャが即不要になる」というのは無さそうで有り得るケースです。GPUがピクセルを転送しようとしたら転送先のテクスチャがなくなっていて不正なメモリアクセスというケースはDX12では発生します。コマンドリストから参照されている以上、テクスチャも忘れずに保護しておきます。

ちなみに、GitHubにあるマイクロソフトのDirectX-Graphics-Samplesではその場でコマンドリストを実行してフェンスで待つようになっています。その為アップロードバッファを生成した同じ関数内で使用済みになりその場で解放できるメリットがありますが、CPUはGPUがテクスチャを作る間ブロックされています。

まとめと課題


DX12のテクスチャの生成で分かり難いのは、アップロードバッファやリソースバリアなどの存在があると思いますが、この辺を隠蔽してかつてのDirectX SDKの感覚でテクスチャを生成できるようにしてみました。ただし今回の実装は、関数の引数が簡潔な反面、コマンドリストやコマンドリストから参照中のアップロードバッファの保持などを外部のモジュールに依存しているので、透明ではなく、そのままマルチスレッド化できないものになってしまいました。テクスチャのアップロード1つ取ってもどんな設計を選択するか、これもまたDX12時代にゲームエンジン開発者に委ねられた課題となりました。

Monday, August 15, 2016

DirectX 12: Resources Barriers and You を要約

MicrosoftのYouTubeチャンネル「Microsoft DirectX 12 and Graphics Education」より、「DirectX 12: Resources Barriers and You」を要約してみました。



リソースバリアの3つの役割

  1. リソースのステートの変更(例:RT=レンダーターゲットからSRVへの変更等)
  2. キャッシュコヒーレンシの確保
  3. パイプラインストール(例:書き込みの後に読み込みをする場合、順序が前後しないことを保証)


パフォーマンスの為の3つのルール

  1. D3D12_RESOURCE_STATE_COMMONとD3D12_RESOURCE_STATE_GENERIC_READステートは避けます。

    GENERIC_READは、D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER、D3D12_RESOURCE_STATE_INDEX_BUFFER、D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE、D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE、D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT、D3D12_RESOURCE_STATE_COPY_SOURCEというフラグを全て含みます。これはリソースがあらゆる場所で使われる可能性があると見なされて多くのパイプラインストールを発生させます。GENERIC_READを使うのは"Upload heap"、すなわち、CPUからGPU側のバッファを更新する時に限るべきです。

    D3D12_RESOURCE_STATE_COMMONを使うべき場面はCPUがテクスチャにアクセスする場合と、"Copy engine"※に渡すCopy queueにリソースを渡す場合に限ります。

    SRVはD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEとD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEの二種類あります。通常は本当に使われるもの一つを指定したほうがよいですが、両方で使われることがわかっていればor演算子で両方指定します。1つだけ指定するのがよいと思ってRT→PS→NON PSのようにステートを渡り歩くのは良くないです。 
  2. 不必要なトランジションは避ける。例えば実装の都合で任意に中間ステートなどを置いたりしないことです。高くつく可能性があります。
  3. 複数リソースのトランジションを纏めて発行してパフォーマンスアップ  ID3D12GraphicsCommandList::ResourceBarrierには複数のバリアを引数として渡せます。例えば DS=depth stencilとRTをそれぞれSRVにするなら2つ同時に指定するべきです。また、複数リソースをSRVからD3D12_RESOURCE_STATE_COPY_DESTに変更して書き換える場合も、個別にResourceBarrierを呼ぶのではなくResourceBarrierの一度のコールで複数のリソースを指定すべきです。

おまけ
Depth bufferはハードウェア内部で圧縮されていて、SRVに変更をするとその時点から解凍作業が始まってすぐに使えません。 そこで、D3D12_RESOURCE_BARRIER_FLAG_NONEの替わりに、D3D12_RESOURCE_BARRIER_FLAG_BEGIN_ONLYとD3D12_RESOURCE_BARRIER_FLAG_END_ONLY の2つに分けて発行して、BEGINとENDの間にGPUに別の計算をさせると、計算と解凍を並行に走らせることができます。

※ "Copy engine"が何なのかわからなくて調べてみたのですが、どうやらここが解説ページです。

https://msdn.microsoft.com/en-us/library/windows/desktop/dn899217(v=vs.85).aspx

モダンなGPUハードウェア内に実装された3つのエンジン"Copy engine"、"3D engine"、"Compute engine"のうちの1つであり、D3D12_RESOURCE_STATE_COMMONステートはその"Copy engine"内部で行われる全ての内部的なステートを内包している、ということのようです。

Thursday, July 28, 2016

[DX12] GetCopyableFootprintsの謎に迫る

DirectX12ではテクスチャを作る時にメインメモリでもテクスチャでもない、中間バッファ(UPLOAD heap)を経由します。その際、ID3D12Device::GetCopyableFootprintsという関数で取得したレイアウトに従ってピクセルを中間バッファに格納します。

GetCopyableFootprints関数と共にいまいち存在意義がわかりにくい中間バッファがなぜ必要なのでしょうか。

答えはMSDNにありました。
https://msdn.microsoft.com/en-us/library/windows/desktop/dn899215(v=vs.85).aspx

テクスチャはキャッシュ効率を上げるためGPU上ではDDSファイルのように一直線(linear)に配置されておらず、non-linearな未知のレイアウト(unknown layout)として隠蔽されています。中間バッファの内容をそのGPUに都合の良い形式に変換してくれるのが、ID3D12GraphicsCommandList::CopyTextureRegionというわけです。

中間バッファに配置するピクセルもまたGPUに都合の良いようにアラインメントを揃える必要があり、そのための定数が定義されています。

  • D3D12_TEXTURE_DATA_PITCH_ALIGNMENT 
  • D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT

MSDNによると、サブリソース(Subresource、ミップマップやキューブマップなどのテクスチャの構成単位)ごとに512バイト境界、テクスチャの各Row(行、つまりX方向一列)は256バイト境界に揃っている必要があります。

GetCopyableFootprintsを呼び出すと、各サブリソースをどのように一次元のバッファに格納すればいいのかD3D12_PLACED_SUBRESOURCE_FOOTPRINT構造体で教えてくれます。つまり、GetCopyableFootprintsはただのヘルパー関数であって、デバイスを操作したりデバイスに問い合わせたりしません。また、GetCopyableFootprintsを使わず自力で配置しても構いません。

中間バッファは二次元や三次元ではない、Constant Bufferなどと同じただの一次元バッファ(D3D12_RESOURCE_DIMENSION_BUFFERで、D3D12_HEAP_TYPE_UPLOAD)として生成するのですが、その中にGPUが読めるようにサブリソースを間違いなく配置する必要があります。

ところで、DDSなどから読み込んだテクスチャは多くの場合既にRowが256バイト境界に揃っています。そこに注目するとこんな手抜きアップローダーも書けます。

assert(rowSizeInBytes == footprint.Footprint.RowPitch) の行でひっかかるテクスチャは横が256バイト単位でない場合です。実はある程度のX方向の大きさがあって、サイズがPower of Twoで、ミップマップを無視するならひっかからないはずです。

(AddIntermediateCommandlistDependentResourceは、コマンドバッファの実行が終わるまで中間バッファを保持しておくためのものです。今回の主題ではないので説明は省略します)

Saturday, July 23, 2016

[DX12] 実例で学ぶ ID3D12Fence ダブルバッファリング編

前回やった、フェンスが指定の値に達するまで待つ関数を再掲載します。


フェンスは通常、渡す値をインクリメントしながら使います。現在GPUに渡したコマンドリストが全て終了するまで待機するには以下のようにします。


fenceValueはアプリケーション開発者が管理します。UINT64型で宣言しておき、インクリメントしながら使います。

DX12ではダブルバッファリングを講じない限り、GPUが参照しているリソースをCPUが壊してしまわないように毎フレームの描画ごとにこれを実行する必要があります。例えば、ID3D12CommandAllocatorなどは、GPUが対象のコマンドリストを実行している間は破棄したり書き換えたりするとData Raceが発生することになります。それを防ぐ為にも上のコードを毎フレームを実行します。

しかし、こうすると毎フレームCPUとGPUがお互いの処理を待つような動作になるためフレームレートが低下します。そこで、GPUの処理中には保持しなければいけないリソースは2つずつ用意することにします。以下のようなクラスを用意すると便利です。

このクラスのインスタンスを2つ作って、現在描画対象のバックバッファ(IDXGISwapChain3::GetCurrentBackBufferIndexで取得)によって参照を切り替えたいリソースを宣言に追加していきます。 fenceValueToGuardは該当フレームに対する描画が終了したときのfenceValueを保持しておくためのもので、フェンスの値がfenceValueToGuardを超えたらこれらのリソースに対するGPU側の処理が終了したと知ることができます。

constantBufferは前々回の記事で書いた巨大なConstant Bufferで、アプリケーションはここから256バイト単位で割り当てながら使うものです。mappedConstantBufferはconstantBufferをMapしっぱなしにしたメモリの先頭アドレスです。これらも2フレーム分作って置きます。ID3D12DescriptorHeapも同様な理由で前々回に動的に割り当てるようにしてあり、同様の理由で二重化するためここに追加します。

これで、Constant Bufferの書き換えに関してGPUを待機するコードがなくなりました…と、言いたいところですが、2フレーム前のGPUの処理が終わっていない場合を想定しなければなりません。

毎回フレームの描画開始前にFrameResourcesに保存しておいたfenceValueToGuardを取得してを呼び出します。ダブルバッファリングしているときはIDXGISwapChain3::GetCurrentBackBufferIndexが0と1が交互に来るので、結果的に2フレーム前のフェンスの値でWaitFenceValueを呼ぶことになります。

フレームの描画終了時には、以下のようにしてコマンドキューとfenceValueToGuard双方にfenceValue値を記録します。

こうすることで、GPUを待つことが完全になくなるわけではありませんが、真のダブルバッファリングが完成しました。

これで、どれだけ速くなるかみてみます。垂直同期をなくすためには、swapChain->Present(0, 0) と、最初のパラメータに0を指定します。うちのGTX 650ではダブルバッファリング前は400フレーム台だったのが500~600フレーム台にまで上がるようでした。

Wednesday, July 20, 2016

[DX12] 実例で学ぶ ID3D12Fence 基本編

DX11以前やOpenGLでは、基本的にCPUとGPU間でのいわゆるData Raceが生じないようにAPIレベルである程度保証してくれますが、DX12では同期を取る事は開発者に委ねられることになります。

そこで、GPUがどこまで実行したのか調べるのに使うフェンスですが、ID3D12Fence、HANDLE、ID3D12CommandQueue、それにUINT64型の変数と登場するオブジェクトが多くて分かり難いです。

結局、大雑把に何をするか書きだすと以下の2点に絞られます。

  • GPUが実行位置に合わせてフェンスの値をインクリメント
  • CPUはフェンスが期待した値になるまで待機する

GPUがフェンスの値をインクリメントと書きましたが、コマンドをGPUに送るのはCPUなのでその値もCPUから指定します。以下のようにコマンドリストの実行に混じって呼び出しているSignalがそれです。

A、B、Cがそれぞれ描画命令等が記録されたID3D12CommandListです。CPUから見て、フェンスの値が1になっていたらGPUでコマンドリストAの実行が終了しています。同様に2ならBが、3ならCの終了が保証されます。

GPUが設定したフェンスの値が指定値以上になるまで待つ関数はこんな感じです。

ここでvalueに1を指定した場合、Aの実行が完了していなければAが終わるまで待機します。Aの実行が終わっている状態、すなわちフェンスの値が1以上になったらすぐに戻ってきます。「1」ではなく「1以上」であることは重要です。Aが終了したかを知りたいためで、Bの実行終了時の「2」であってもやはりすぐ戻ってこなければいけないからです。

また、Signal呼び出しは常にvalueをインクリメントしながら行うのが定石で、その値を管理するためUINT64型の変数を用意することになります。

同期に使うHANDLEは使い捨てにしています。生成と破棄のオーバーヘッドが気になるなら保持しておく方法もありますが、何か意味のある状態をグローバルに保持するかのように見えるため可読性が落ちる気がします。個人的には使い捨てがおすすめです。
DirectX-Graphics-Samplesでは保持するように書かれています。

上のWaitFenceValue関数は必要最低限のコードでしたが、通常はGetCompletedValueを組み合わせて使うようです。待機する必要が無い場合を切り分けると少しパフォーマンスが上がる為と思われます。

Saturday, July 9, 2016

DX12でGPUメモリを自分で管理する

今回のソースは https://github.com/yorung/dx12playground/tree/huge_global_constant です。

問題提起


前回シンプルなRoot Signatureをやってみましたが、毎回書き換えるConstant bufferのアドレスを固定してしまうのは何かと不都合です。

DX11までは度々以下のようなコードで描画することがありました。

見た目のConstantBufferは1個なのですが、内部的なConstant Bufferがobjの数だけ確保され、それぞれ別のアドレスを持ちます(リネーム)。もし内部でも見た目通り1個のConstant Bufferしかなければ、Mapする時に前回のDrawが終わっていなければいけません。前回のDrawを待つなら遅くなりますし、待たなければGPUが参照中のメモリを壊す事態となります。

DX12で同じことをやるとGPUが参照中のメモリを壊してしまいます。複数のobjを描画するためにはobjの数だけConstantBufferを確保しなければならず、結局Constant bufferはアドレスを変えながらバインドしなければなりません。そうなると、Constant bufferのアドレスを保持するDescriptor Heapもまたバインドの数だけ必要になります。


GPUメモリのアロケータを作る


そこで、Constant BufferとDescriptor Heapを動的に確保するシステムを作ります。 

DX12のいいところは直接GPUのアドレスを指定できることです。巨大なConstant BufferやDescriptor Heapを1個ずつだけ作って、その中から適当な先頭アドレスをGPUに渡して描画、ということが出来るようになりました。

以下はその唯一のConstant Bufferを作るところです。


Constant Bufferの先頭アドレスとサイズは256バイトのアラインメントの制限があります。ここでは256 x 1000バイト確保しました。また、MapしっぱなしにしてmemcpyだけすればGPUから変更が見えているというのもDX12のいいところです。

次は、唯一のDescriptor Heapを作ってみます。


1024個作ってみます。個数に特に意味はありません。
使用者はここから必要な数だけ確保し、CBVやSRVを書き込んでAPIに渡します。


AssignDescriptorHeapが確保関数で、先頭から順に返しているだけで、足し算しかしていません。使用者は「先頭から何番目を何個」という情報を知っているだけで十分だからです。ちなみに、numAssignedSrvsは毎フレーム0にリセットしています。

AssignSRVは確保したDescriptor HeapにSRVを書き込みます。CreateShaderResourceViewはDX11と名前が同じですが、何か新たにインスタンスが生成されたりするわけではなく、書き込むだけです。

最後に、SetAssignedDescriptorHeapでDescriptor HeapをGPUに渡します。今回のサンプルも前回のように単純化のためにDescriptor Tableは1個だけということにしています。descriptorHeapIndexを先頭位置として、何個のSRVやCBVを参照するかはDescriptor Tableの定義次第です。

以下はSRVの代わりにConstant Buffer開始位置をDescriptor Heapに書き込み、更にそれも唯一のConstant Bufferから動的に確保したものを使う関数です。

唯一のConstant Bufferから256バイトにアラインメントされた単位で先頭から確保しますが、これもただの足し算です。渡されたデータを書き込むのはmemcpyです。これでGPUから見えます。
最後に、CreateConstantBufferViewでDescriptor Heapに先頭アドレスを書き込んで完了です。

実際の描画コードはこんな感じです。

1個だけのDescriptor Tableは2つの連続したDescripor Heapを参照し、最初がCBV、次がSRVになっています。使用者がGPUメモリを管理する手間が減ってコードがすっきりしました。

まとめ


GPUメモリのアロケータをやってみました。

こうしてみると、最後にGPUに渡すのはGPUのアドレスになっていて、GPUメモリの管理方法はほぼアプリケーション開発者に委ねられているのがわかります。

上では複数オブジェクトを描画するときのDX11のリネームを例にあげましたが、単一のオブジェクトを描画していても次のフレームのDraw Callの時点で前のフレームをまだGPUが処理していた場合、リネームが発生します。
つまり、DX12ではダブルバッファリングやトリプルバッファリングを行う際にレンダーターゲットだけでなくコンスタントバッファもプログラマの責任で多重化します。

今回の作業はDX12を使ったダブルバッファリングやトリプルバッファリングの下準備でもあります。

今回はシンプルに実装するために全てを動的に確保するようにしましたが、マテリアル情報のような変化しないConstant Bufferを変化しないDescriptor Heapに入れるなど、変化の有無でDescriptor Tableを分けるような工夫が必要になりそうです。