2023年4月30日日曜日

Enhanced Barriers

新しいバリアが使えるようになったので、ライブラリを書き換えてみた。


D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS


新しいバリアの説明を読んで、このフラグの存在を知った。
勝手に状態遷移をして、その条件が複雑で分かりづらいと古いバリアの欠点として書かれていた。

今まではこのフラグに頼らず、自前でリソースの読み込みと書き込みが切り替わるポイントで、自動的にリソースバリアを挿入するようにしていた。
だけどこのフラグを使うと一部のリソースを除いて、自動的に遷移してくれるし便利そう。

新しいバリアでも使えるみたいなのでこのフラグを立ててすべてのバリア処理を消してみた。


バッファ


まずリソースの生成関数が変わり、CreatePlacedResource2になった。
以前すべてのリソースをCreateCommittedResourceから、CreatePlacedResourceに変えたけど、今回はCreatePlacedResource2に変わる。
引数の変更点は、D3D12_RESOURCE_DESCからD3D12_RESOURCE_DESC1に変わる(中身はSamplerFeedbackMipRegionが増えた)のと、初期のリソースステータスを渡していたのを、初期のD3D12_BARRIER_LAYOUTを渡すようになったこと。

D3D12_BARRIER_LAYOUTでいうと、バッファに使うのはD3D12_BARRIER_LAYOUT_UNDEFINEDでいいらしい。
前の場合は、UploadとReadbackの場合は、D3D12_RESOURCE_USAGE_GENERIC_READで、それ以外はD3D12_RESOURCE_USAGE_COPY_DESTで始めていた。

そして、D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESSを指定することによって、バッファの場合は遷移を何もする必要がない。
書き込む時にD3D12_RESOURCE_STATE_GENERIC_READにして、コピーする時D3D12_RESOURCE_STATE_COPY_SOURCEにして、使うときにそれぞれのステータスにわざわざ遷移させていたけど、新しいバリアでは?それともD3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESSのフラグのお陰で?何もしなくて良くなった。

というわけでバッファについてはバリア処理が0になった。

テクスチャ


テクスチャの方はバッファのようにバリア0にはならなかった。
ただ、単純なファイルから読み込んだテクスチャはバッファ同様バリア0で行ける。
初期のレイアウトはD3D12_BARRIER_LAYOUT_COMMONで、後は勝手に遷移してくれる。
自分で制御しないといけないのは、D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGETとD3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCILを指定する場合で、このフラグを付ける場合はD3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESSが付けられない。なので、主にこのリソースについて、書き込み時とテクスチャ参照時にバリアを使うことになる。


グローバル


新しいバリアの3つ目のグローバルバリアについてはよくわからない。

Global barriers control cache flush and synchronization for all indicated resource access types in a single command queue. Global Barriers have no effect on texture layout. Global Barriers are needed to provide functionality similar to legacy NULL UAV barriers and NULL/NULL aliasing barriers.

説明にはこう書かれているが、全く理解できない。とりあえずは置いておく。


中間レンダーターゲット


中間レンダーターゲットはD3D12_BARRIER_LAYOUT_RENDER_TARGETスタートで、テクスチャとして使う場合に、D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_SHADER_RESOURCEに変える。


深度バッファ


深度バッファはD3D12_BARRIER_LAYOUT_DEPTH_STENCIL_WRITEスタートで、テクスチャとして使う場合に、D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_SHADER_RESOURCEに変える。


レンダーターゲット


レンダーターゲットは直接リソースを作らないけど、LayoutBeforeを設定するために初回のレイアウトはD3D12_BARRIER_LAYOUT_PRESENTを設定しておく。
書き込む際はD3D12_BARRIER_LAYOUT_RENDER_TARGETに変える。

新しくAccessBefore/AfterとSyncBefore/Afterが増えた。
この指定がいまいち理解できていないが1つわかった事がある。


実際には良くない例だけど、Presentを呼ぶ前にレンダーターゲットの遷移のみのExecuteCommandListsを実行していた。
この時、下記のように指定。
 AccessBefore=D3D12_BARRIER_ACCESS_RENDER_TARGET
 AccessAfter=D3D12_BARRIER_ACCESS_COMMON
 SyncBefore=D3D12_BARRIER_SYNC_RENDER_TARGET
 SyncAfter=D3D12_BARRIER_SYNC_ALL
特にSyncAfterは何を指定していいのか全く検討がつかない。

実行してみると、デバッグレイヤーにはこんなメッセージが出ていた。

D3D12 WARNING: ID3D12CommandQueue::ExecuteCommandLists: ExecuteCommandLists references command lists that have recorded only Barrier commands. Since there is no other GPU work to synchronize against, all barriers should use AccessAfter / AccessBefore = D3D12_BARRIER_ACCESS_NO_ACCESS and SyncBefore / SyncAfter = D3D12_BARRIER_SYNC_NONE. This information can be used as an optimization hint by some drivers. [ EXECUTION WARNING #1356: NON_OPTIMAL_BARRIER_ONLY_EXECUTE_COMMAND_LISTS]

バリアのみのコマンドリストは何も同期するものも無いからD3D12_BARRIER_ACCESS_NO_ACCESSとD3D12_BARRIER_SYNC_NONEをBefore/Afterに設定すればいいとのこと。
なるほど、今までExecuteCommandListsの枠を超えて、その前のコマンドも含めて考えないといけないのかと思っていたけど、今回の実行単位の中だけで考えればいいということがわかった。

コマンドリストの先頭で遷移させる場合はBeforeにD3D12_BARRIER_ACCESS_NO_ACCESSとD3D12_BARRIER_SYNC_NONEを設定出来るし、次の為にコマンドリストの最後でLayoutだけ変えておくような場合、AfterにD3D12_BARRIER_ACCESS_NO_ACCESSとD3D12_BARRIER_SYNC_NONEを設定する事が出来る。


UAV


テクスチャをロードした後、ミップマップを自動でつくるときにミップマップの階層分UAVを用意して、1つ上の階層を参照して、下の階層を作るというのを順番に行う。
この時PIXのヒントに出ていたのは、「UAVバリアが要らなさそうだから消してみれば?」という提案。
実際に消すと、同期が取れていないのでテクスチャがきちんと書き込まれなくなって真っ黒のテクスチャが出来上がる。
新しいバリアではUAVバリアというものが無くなったので、どうするのかと思ったらドキュメントには下記のように書かれていた。
 LayoutBefore = D3D12_BARRIER_LAYOUT_UNORDERED_ACCESS
 LayoutAfter = D3D12_BARRIER_LAYOUT_UNORDERED_ACCESS
 AccessBefore = D3D12_BARRIER_ACCESS_UNORDERED_ACCESS
 AccessAfter = D3D12_BARRIER_ACCESS_UNORDERED_ACCESS
 SyncBefore = D3D12_BARRIER_SYNC_COMPUTE_SHADING
 SyncAfter = D3D12_BARRIER_SYNC_COMPUTE_SHADING
実際にやってみると自分が使っている場面ではエラーがでて、レイアウトを両方ともCOMMONに変えたらうまくいった。
PIXで確認してみたところ、古いバリアの場合各階層ごとに待ちが発生して結構時間がかかっていたけど、新しいバリアではその待ちが消えていた。結果全体で200usぐらいかかっていた処理が150usぐらいに短縮された。新しいバリア恐るべし。


今回の修正でリソースバリアをライブラリで挿入していた処理を一切なくして、今のところ必要な場面で自分で指定する形に変えた。
もう少し新しいバリアを理解して、このまま行くのか、自動で挿入するかを見極める。


2023年4月24日月曜日

DirectX12 Agility SDK

Agility SDKをインストールはしたけど、結局使えなかった。
ID3D12Device10を指定して、D3D12CreateDeviceをしてもエラーになる。

ここを見つけて、ついにID3D12Device10のCreateに成功した。


NuGetでのインストールは簡単で、NuGetパッケージマネージャから「DirectX agility」で検索してインストールするだけ。


現在の最新は1.610.2で、1.7を使いたい場合は上部の「プレリリースを含める」をチェックしないと選択できない。

インストール後、ビルドするとexeの場所に「D3D12」というフォルダの中に、「D3D12Core.dll」と「D3D12SDKLayers.dll」が配置される。
Agility機能を使ったプログラムをリリースする場合はこの「D3D12Core.dll」を一緒に配布すると、配布した「D3D12Core.dll」を使ってくれるけど、OSのアップデートが進んでSystem32の下の「D3D12Core.dll」の方が新しくなると、新しい方の「D3D12Core.dll」を読み込むようになっているらしい。


肝心のD3D12CreateDeviceだけど、実行するとエラーになる。
実行時のコンソールを見ると、System32の方の「D3D12Core.dll」が読み込まれている。
先程のページを読み進めると、「D3D12SDKVersion」と「D3D12SDKPath」をExportする必要があると書いてある。

extern "C" { __declspec ( dllexport ) extern const UINT D3D12SDKVersion = 710 ; }
extern "C" { __declspec ( dllexport ) extern const char * D3D12SDKPath = R"(.\D3D12\)" ; }

書いてある通り宣言してみる。
でも、System32の方が読み込まれる。
宣言する場所は3DライブラリのDLLのソースに記述していたのを、Exeのソースに移すと直下のD3D12のDLLが読み込まれるようになった。

これでID3D12Device10のCreateに成功して、CheckFeatureSupportのD3D12_FEATURE_D3D12_OPTIONS12も正常に返るようになった。

ついに拡張バリアを試すときが来た。



Flustum Culling 影考慮版

視錐台カリングを以前作ったけど、不具合があることに気がついた。

オブジェクトのカリングと影の描画が連動しているため、カリングされてしまうと影の描画も無条件でやめてしまう。
影の位置が離れている場合や長く伸びている場合、オブジェクトの描画をやめても影の描画はしないといけない場合もある。

影の描画がなくなっている

正しい描画

アンリアルエンジンのサイトでオブジェクトをカリングした状態でも影描画している様に見えた。影はカリングに関係なく無条件に描けばいいのか?

とりあえずその実装をしてみることにしたが、単純にはできなさそう。
現状メッシュ描画の際、オブジェクトのインスタンス単位にWorld Matrixのバッファを用意して、バッファの個数をDrawIndexedInstancedのインスタンス数に指定することで描画している。カリングされなかったものだけバッファを詰めて、インスタンス数を減らして指定する。
このため、単純に影用のバッファを含めてしまうとカリングもできなくなってしまう。

そこで考えたのが、バッファを2重にする方法。
カリングされないバッファの後ろにカリングされた影描画分を詰める。
影描画は全体の数を使って描画し、それ以外はカリングされていない数を使って描画する。
これにより1つのバッファでカリングと全体描画が出来るようになった。

出来るようにはなったけど、これだとパフォーマンスの問題が残る。
広いエリアで全オブジェクトの影描画が行われるとまずいだろう。

視錐台カリングの影考慮を検索してもなかなかヒットしなかったが、いろいろ試していく中で1つ有力な、目からウロコの情報が手に入った。
現状、メインカメラで視錐台カリングを行っているが、影描画用のライトをカメラに見立てて、その視錐台でカリングを行うというアイディア。
全然情報が出てこなかったけど、実は常識?


カスケードシャドウ対応視錐台カリング


カスケードシャドウを行う際、カスケード数分視錐台を準備している。
この視錐台をそのまま視錐台カリング処理に流し込めば、影描画の範囲を絞り込める。
また、カスケード数が4だとして、4回影描画を行うことになるがそれも狭い範囲から段々と数を変えて描画出来るようにする。

左がメインカメラ 右がサブカメラで上からの視点

まず、メインカメラの視錐台でカリングを行う。この結果をほとんどの描画では利用する。
次にカスケードシャドウ用の視錐台を範囲の狭い方から含まれるオブジェクトのチェックを行う。チェックの対象はメインカメラでカリングされたものだけ。
手前から赤、黄緑、青、黄色と影の範囲が広くなっていく。
それぞれの範囲内に含まれるオブジェクトにカリングレベルを設定していく。

影描画の際、今までは全部のカスケードレベルで同数のメッシュ描画を行っていたけど、カリングレベルを使って、その範囲に含まれるメッシュだけを描画するようカスケードシャドウ自体のパフォーマンスにも考慮させた。

左上の3✕3の物体と影

メインカメラで物体はカリングされても、影は残っている





2023年4月23日日曜日

DATA_STATICなTexture

テクスチャを読み込んでミップマップを自動的に作るときは、UAVを経由で書き込むためD3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESSのフラグを付けてリソースを作っていた。

ただ、リファレンスにはUAVを殆ど使わない場合はコピーして使うのを検討したほうがいいと書いてあり、このフラグのコストが高いことが伺える。

このケースでは確かにUAVは最初のミップマップ生成の為だけに使っておりUAV無しのリソースにコピーしたほうが良さそうだ。

毎フレームレンダーターゲットのミップマップを作ってテクスチャとして利用するような場合は、UAV付きのままで使っても良さそう。
実際の処理時間を計測してみるとコピーした方が20~30usぐらい時間が伸びていて、「UAV付きテクスチャを参照のコスト < Copy+UAV無しテクスチャ参照のコスト」になる為、毎フレームコピーするのは逆にもったいない結果になっていた。


CopyTexture


CopyTextureコマンドにサブリソース番号を追加してミップマップの各階層もコピーできるように拡張し、要求側ではミップマップの階層分ループしてコマンドを発行するように修正。

今まではサブリソースインデックス0固定だったものをインデックス指定に変えただけなので特に問題なくできた。


CreateTexture


テクスチャリソース作成関数内でテクスチャをロードしてバッファをコピーするとき、一時的なUAV付きリソースにコピーするように変更。
そのリソースでミップマップを生成して全て出来上がったら、全体をCopyTextureしてUAV付きのリソースは破棄。


実行して試してみたところ問題なく動く。
ただいつもこういった修正後に、デバッグ実行するといつもデバッグレイヤーに何かが表示されている。

今回もやっぱりエラーが出ていた。

D3D12 ERROR: ID3D12CommandQueue::ExecuteCommandLists: Resource(0x000001C4E398C710:'Mesh<MeshAlbedoTex1>') (subresource : 8) is bound as DATA_STATIC on Command List 0x000001C4E34C5510:'DirectCL1'. Its state was changed by a previous command list execution which indicates a change to its data (or possibly resource metadata), but it is invalid to change it until this command list has finished executing for the last time. [ EXECUTION ERROR #1002: DATA_STATIC_DESCRIPTOR_INVALID_DATA_CHANGE]

イマイチよくわからないけど、テクスチャのコピー完了を待たずに次々に処理しているからかと思って、完了を待つようにしたりしたが状況は変わらず。

このメッセージが出力されるタイミングはテクスチャ読み込み時ではなく、テクスチャを描画するタイミングで出力されている。また、一旦エラーが出ると毎フレーム出ることが普通なのに今回のケースでは、最初の1回だけで以降は出ていない。
描画タイミングまでコピーが終わってないと言うのは考えられないし何のことを言っているのか再考してみると、DATA_STATICというのと、最初の1回だけ出力されるというのがヒントになった。

シェーダのルート署名では、純粋なテクスチャの場合DATA_STATIC、レンダーターゲットなどをテクスチャとして使う場合は、DATA_STATIC_WHILE_SET_AT_EXECUTEを指定している。

テクスチャのその前のステータスはCOPY_DESTになっている。
今までUAV付きのテクスチャではこのエラーは出ていなかったが、今回UAVなしになったことでDATA_STATICの恩恵を受けられるようになったからなのか、チェック対象になって怒られるようになったとか?

リソースの遷移は、はじめてライブラリを作った頃はマイクロソフトのサンプルの通り使う状態に遷移させて元に戻すということをしていたが、今は使う場面の状態に合わせて遷移させてそのままという風にしている。そのため、各リソースの作成直後はだいたいCOPY_DESTのままになっている。

DATA_STATICを宣言しているシェーダの実行中にテクスチャの状態遷移を行ってしまっているのがだめだと予想して、コピー直後にステータスの遷移まで完了させてみた。こうすることで以降このテクスチャに対して状態遷移は発生せず、まさにSTATICって感じになる。

この状態で試してみるとエラーは消えて動作するようになった。


2023年4月20日木曜日

DirectX12 Multithread対応

いままでライブラリを構築してきたけど適当なデータ試した感じでは、シングルスレッドでなんの不満もなかった。ゲームの世界のスピード感覚が無いので、マイクロ秒、ナノ秒単位で処理が終わってるし十分速いと思っていた。
また、マルチスレッドといっても、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で失敗する。しばらく待つしかないのかな?