いままでライブラリを構築してきたけど適当なデータ試した感じでは、シングルスレッドでなんの不満もなかった。ゲームの世界のスピード感覚が無いので、マイクロ秒、ナノ秒単位で処理が終わってるし十分速いと思っていた。
また、マルチスレッドといっても、CPUとGPUで別々に動いている時点でマルチプロセッサになってるし、GPU内に処理を任せると内部の大量のスレッドで処理をしてくれるため、何をスレッド化するのかよくわからない。
DirectX12ではマルチスレッドにしないと、DirectX11に比べて寧ろ遅くなるという情報を見つけた。
気になったので、マイクロソフトのサンプルD3D12Multithreadingを確認してみた。
D3D12Multithreading
このサンプル内ではワーカスレッドを3つ用意して、それぞれのスレッドに影とシーンの描画のため、コマンドリストにDrawIndexedInstancedを追加させていた。
1つの頂点バッファにシーン内のオブジェクトがすべて含まれているようで、それぞれのマテリアル単位でDrawコマンドを追加しているっぽい。
1シーンに必要なDrawコマンドの発行回数が1024×2(影とシーン)となっていた。
これだけのDrawコマンドを発行する場合、1スレッドで追加するのと複数スレッドで追加する場合で処理スピードが変わってくると言うことか。
今は数千ポリゴンのメッシュを6回に分けて描画しているだけだったので大して問題を感じていなかったけど、実際のゲームのシーンになってくると段々コマンドリストへの追加処理自体が問題になって来るのだろう。
というわけで、ライブラリをマルチスレッド対応することにした。
コマンドリスト
マルチスレッド化するにあたり、コマンドリストの配置場所が問題になる。
今まではシェーダクラス内にコマンドリストとアロケータを用意して、シェーダを中心に処理をしていた。
ワーカスレッドを用意する形にする場合、各シェーダに用意するのは問題だろう。
そこでコマンドリストとアロケータを引き剥がして、コマンドリストクラスを用意することにした。
まず初期化だけど、CreateCommandListの引数にID3D12PipelineStateのポインタを渡す必要がある。汎用的に使うのにどうしたら良いかと思ったらnullptrを渡せた。コピー専用の場合に使ってた。もうちょっと調べたらCreateCommandList1というID3D12PipelineStateを渡す必要もなく、しかもClose状態で始まるぴったりな関数が見つかった。
次にBegin、Endの関数を追加してこの区間でコマンドを追加出来るようにする。
Beginではシェーダを受け取ってコマンドリストをリセットする。
Endでは今まで受け取ったコマンドのリソースをまとめて状態遷移させるように、先頭のコマンドリストにResourceBarrierを発行するようにした。
今までまとめて状態遷移させるためにすべてのリソースを横断的にまとめて状態遷移させるように専用コードを書いていたけど、リソースが追加される度に変更が必要になる。
これを単純にコマンドを追加するだけで自動的に行なってくれる仕組みができた。
これでコードがスッキリするし、余計なことを考えなくても状態遷移がまとまって問題解決かというと、実はそうでもなかった。
中間レンダーターゲットに描画して、その描画結果をテクスチャとして利用する場合、同じリソースに対し状態遷移が複数発生し、後勝ちで思い通りの遷移にならない。同じリソースが出てきたら最初の遷移だけをするようにして、後は必要なタイミングで遷移する様にした。
スレッド指定
今まで1つのコマンドリストに実行してほしい順番にコマンドを追加していた。
マルチスレッドになっても同じで、例えば4つのコマンドリストと4つのワーカスレッドを用意して、1番目のコマンドリストから実行してほしい順にコマンドを追加して、2番目,3番目,4番目と順番に実行していってくれるものと思っていた。
ExecuteCommandListsの引数には、コマンドリストの数とリストの配列を渡すようになっている。
とりあえずできたので、実行してみるとものすごくチカチカする。
コマンドリストを複数使う場合、1つで使っていたコマンドを振り分けるだけではうまくいかないみたい。
チカチカした原因は、それぞれのコマンドリストで必要最低限のコマンドが存在して、その前のコマンドリストで実行したからといって、後続のコマンドリストでその影響が必ずしもあるわけではなかった。リソースに対するコマンド(ClearRenderTragetなど)はもちろん後続のコマンドリストでも有効だが、コマンドリスト自体に対するコマンドはそれぞれのコマンドリストで実行が必要と理解した。
最低限必要なコマンドは下記だった。
- SetGraphicsRootSignature(Direct/Compute)
- SetDescriptorHeaps(Direct/Compute)
- OMSetRenderTargets(Direct)
- RSSetViewports(Direct)
- RSSetScissorRects(Direct)
そこで、コマンドをどのコマンドリスト(スレッド)に追加するかを指定出来るようした。
※上記はDirect/Computeコマンドの場合で、Copyで必要なコマンドはない。
FirstThread
最初に実行する必要のあるコマンド追加。
ClearRenderTragetなど、最初に1回だけ実行するようなコマンドで利用する。
LastThread
最後に実行する必要のあるコマンド追加。
ResourceBarrierなど、最後に1回だけ実行するようなコマンドで利用する。
CommonThread
共通で実行する必要のあるコマンドを追加。
上記のSetGraphicsRootSignatureなど、必要最低限のコマンドはこれで追加すると全スレッドに追加されるようにコピーする。
ParallelThread
並列に実行できるコマンドを追加。
ただ、それぞれのコマンドを一旦キューに追加して、各スレッドがキューから取り出しコマンドリストに追加してもまともに描画されない。
順番にキューに追加されたものを、各スレッドがバラバラのコマンドリストに追加してしまうからだ。
例えばメッシュをレンダリングする際、下記のコマンドを追加する必要がある。
- IASetPrimitiveTopology
- IASetVertexBuffers
- IASetIndexBuffer
- SetGraphicsRootDescriptorTable
- DrawIndexedInstanced
そこで順番が関係するいくつかのコマンドをまとめて1コマンドとする仕組みを用意して、その単位で追加できるようにした。
結果
マルチスレッドにした結果、殆ど変わらなかった。むしろ遅くなってるくらい。
1つのスレッドで十分な場合など、最適化したが全体の時間は変わらず、ExecuteCommandLists単位の隙間がやたら長くなっている気がする。
![]() |
| マルチスレッド |
![]() |
| シングルスレッド |
![]() |
| マルチスレッド版コピー部分 |
![]() |
| シングルスレッド版コピー部分 |
最初のコピーはシングルスレッドの方が明らかに速い。
ただこれは数の問題で、3つのBufferコピーをワーカスレッドで1つずつ追加している。規模が大きくなってそれぞれのスレッドが10とか50とかの単位で追加する必要が出てくるとマルチスレッドの効果が出てくると思われる。
![]() |
| マルチスレッド版カスケードシャドウ部分 |
真ん中のカスケードシャドウはマルチスレッドの方が速くなっている。
この様に数がある程度あると効果があるっぽい。
ただし、終わった後の次の処理が始まるまでがやたらと長くて、結局シングルスレッドと同じになってる。
この間隔は状態遷移みたいなので、やり方に問題があるのか?
D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESSを使うと、自動的にステータスを遷移してくれるらしく、COMMONの状態から必要な状態に遷移して更に戻ってくれるらしい。ただし、深度バッファは対象外なのと書き込みから読み込みなどの遷移は自分でやる必要がある模様。
後は新しいバリアが出来るらしく、今までのだめなところがいろいろ改善されるみたい。
DirectX 12 Agility SDK 1.7以降で使えるようになるらしい。
NuGet経由で簡単にインストール出来るので試してみたけど、ドライバが対応してないのか、CheckFeatureSupportでD3D12_FEATURE_D3D12_OPTIONS12をチェックしても未サポートだった。また、実際に使うにはID3D12Device10にする必要があるけど、D3D12CreateDeviceで失敗する。しばらく待つしかないのかな?








0 件のコメント:
コメントを投稿