2022年8月2日火曜日

Unordered Access View(UAV)

DirectX12でプログラムを組む際、扱うリソースのD3D12_DESCRIPTOR_HEAP_DESCを用意することになる。
タイプがいくつもあって最初はなんのことかよくわからない。

D3D12_DESCRIPTOR_HEAP_TYPE_RTV

RenderTargetView。レンダーターゲット用。描画対象になるリソース分用意する。
ダブルバッファリングする場合は画面用に2つ、ポストエフェクト用に中間レンダーターゲットを用意する場合はその枚数分とか。

D3D12_DESCRIPTOR_HEAP_TYPE_DSV

DepthStencilView。深度バッファ用。深度バッファは理解して使えるようになったけど、ステンシルってなんだ?

D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER

Sampler。サンプラー用。ルートシグネチャにStaticでついてるサンプラ1つで十分だろと思ってたときがありました。今では複数使う理由がわかり、必要なサンプラー分用意する。

D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV

ConstantBuffer、ShaderResource、UnorderedAccess View。定数バッファ、シェダーリソース、順序付けられていないアクセス用。
定数バッファは、動的にシェーダにパラメータを渡すためのもの。
シェーダリソースはこの名前だとわかりにくいけど、最初に扱うのはテクスチャだろうからテクスチャと思っておけばいい。あとから普通のデータもテクスチャのレジスタで渡せることに気付く。
順序付けられていないアクセスってなんだ?最初にライブラリを作ったときにはUAVは謎なので扱わなかった。

計算シェーダというものがあり、レンダリングはしないんだけどGPUの豊富なスレッドに一気に計算せさて結果を得られる仕組みがある。別にピクセルシェーダとか他のシェーダでも書き込めるんだろうけども。
その結果を格納するバッファがこのUAVで、Unorderedというのは複数スレッドが順不同に処理するので、その時に同時に書き込めるよという意味だと解釈した。

初めて見たDirectX12のサンプルはD3D12Fullscreen
何故かsceneとpostで2つ用意しているし、更にダブルバッファリングで配列で2つになっている部分もある。たいしたことをしていないのに複雑な構成になっている。
今では、sceneは中間レンダーターゲットに対してレンダリングする用で、postが画面(レンダーターゲット)に描画する用と理解できたが最初は戸惑った。

それでも、疑問点が残ってる。
定数バッファを使いたいと思った時、フレーム数分の領域を用意する必要がある。
50バイトのデータを使う場合はダブルバッファリングなら100バイト分・・・ではない。
定数バッファの最低単位は256バイトなので50バイトしか使っていなくても、512バイト分必要になる。
リソース自体は1つで連続したデータで用意して、2つのデスクリプタヒープでそれぞれの先頭ポインタを指すイメージ。
バックバッファが0番なら、CPU側で0バイト目からデータを書き込んで、GPU側は256バイト目からのデータを参照し、スワップ後バックバッファが1番になったとき、CPU側は256バイト目からデータを書き込んで、GPU側は0バイト目からを参照する。
お互いに並列で処理していく為に2つ必要なのだと理解した。
ただ、今回リソースライブラリをリファクタリングする際、ReadOnly属性のものも作れるようにした。1つ分のバッファでデスクリプタヒープも1つ分。値が動的に変わらないのであれば、2つ用意する必要はない。

ここからが疑問点なんだけど、SRVはどうなのか?サンプルでは中間レンダーターゲットは1つしか用意してないし、同じものに書き込んでいる。レンダーターゲットは2つでバックバッファ用意しているのに、中間レンダーターゲットいらないのか?
この点を某質問サイトで質問してみたが誰からも回答はなかった。
マイクロソフトのサンプルなので、SRVは1つで行けると思っておく。

ではUAVについてだけど、フレーム毎に書き換わるようなデータであればフレーム数分用意する必要があると判断した。リファレンスに丁度そのサンプルが書かれていたのを見つけた。
定数バッファと違って、UAVはリソースから2つ用意する必要がある。
そのリソースを使って、CreateUnorderedAccessViewを呼ぶんだけどこの引数がまた謎。
4つ引数があり、2つ目以外は他と同じなんだけど、2つめのpCounterResourceの意味がわからない。
最初別のネット記事を参考に実装していたときは、ここをnullprtにしていたので無視しておけばいいのかと思っていたけど、リファレンスのサンプルを見つけたので実装の仕方はわかった。別のリファレンスではこのカウンターの利用方法も書かれている。
全然理解は出来ていないけど、同期のための仕組みで用意しておくと内部で勝手に使われて、ある状態のときにはこのカウンタのリセットも必要なると解釈し、一応用意しておくことにした。

カウンタを使う場合は、データの単位が4096になる。
nSize = (nSize + 0xFFF) & ~0xFFF ;

サイズをこれで調節してリソースを作成する。
定数バッファでも256単位にする必要があり、よく構造体にダミーデータを入れて調整しているのを見るけどデータが変わる度に調整が必要だしこっちの方がおすすめ。

0 件のコメント:

コメントを投稿