ClearUnorderedAccessView*の使い方

参考資料

なにをするためのものか

Texture(RenderTarget)のクリアはRTVを通じて、ClearRenderTargetView()を使う方が効率的です。DepthBufferはDSVを通じてClearDepthStencilView()でクリアする事が強く推奨されます。 では、ClearUnorderedAccessView*メソッドが使われる場合ですが、一般的にはCreateCommittedResource()やCreatePlacedResource()で作成したBuffer(VertexBufferやIndexBuffer、またUAVを通じてアクセスする汎用的なBuffer)をクリアするためのメソッドです。

Syntax

APIインターフェースは以下の様になっています。

void ClearUnorderedAccessViewUint(
  D3D12_GPU_DESCRIPTOR_HANDLE ViewGPUHandleInCurrentHeap,
  D3D12_CPU_DESCRIPTOR_HANDLE ViewCPUHandle,
  ID3D12Resource              *pResource,
  const UINT [4]              Values,
  UINT                        NumRects,
  const D3D12_RECT            *pRects
);

大変手間のかかることに、GPU_DESCRIPTOR_HANDLEとCPU_DESCRIPTOR_HANDLEを指定しなければなりません。当然ながらこれらにはクリア対象リソースの全部又は一部の領域を設定したUAVが正しく記述されている必要があります。 また、このメソッドでクリアしなくてはならないリソースの99.99%は、いわゆる1DBufferで、2次元の概念を保持していませんが、クリアの範囲をRECTで指定する必要があります。また、クリアの際に書き込む値はUINT[4]となっています。 初見では疑問しか沸かないこのAPIインターフェースについて少し考えてみたいと思います。

なんでUAVが二つもいるの?

このメソッド最大の面倒な点は、CPU側のUAVとGPU側のUAV二つを用意しなくてはならないところです。ちなみにGPU側のDescriptorHeap(つまりShader Visible Descriptor Heap)にも有効なCPU_DESCRIPTOR_HANDLEはありますが、 これをこのメソッドの引数で渡すことはできません。CPU側のDescriptorHeap(つまりShader VisibleではないDescriptor Heap)を用意してUAVを設定して、そのCPU_DESCRIPTOR_HANDLEを引数で渡す必要があります。

では、なぜこの二つのUAVが必要なのかというと、このクリア作業のコマンド構築をどのように行うかを考えると少しだけ理解できます。 まず、GPUへのコマンド構築を、対象リソースのアドレス解決を含めて行う場合は、CPU側のDescriptor Heapに設定されたUAVを参照することで、キャッシュの効いた高速なメモリから情報を取得出来ます。 一方で、GPU側にはUAVのアドレスとRECTのみを伝えるシンプルな形でのコマンド構築を行う場合は、GPU側から参照可能なUAVが必要となります。この二つのうちどちらが行われるかは、GPUの実装依存となります。

しかし、ClearRenderTargetView()やClearDepthStencilView()はGPU側のDescriptorを必要としませんが、クリア作業はGPU側で行われます。 つまり、コマンド構築時にUAVに相当する情報をコマンドバッファに書き込んでいるわけです。ClearUnorderedAccessView*()も同様の作りで問題なかったのではないかと思います。

RECTでクリア範囲を指定?しかも4要素?

UAV全域をクリアする場合は、RECTによる範囲指定は必要ありませんが、UAVの領域の一部をクリアする場合はRECTを指定する必要があります。RECTは(left, top, right, bottom)を指定する形式になっています。 クリア対象はテクスチャでない場合が多いにも関わらず、RECT指定なのは、単純にClearRenderTargetView()やClearDepthStencilView()のAPIに引きずられたためと思われます。また、RECTの範囲は(left, top) で指定した位置は含みますが、(right, bottom)で指定した位置を含みません。
例えば、R32UINTの16Byte(つまりR32UINT x 4)のUAVがあるとします。このUAVの先頭8Byteをクリアする場合は、RECTの指定は、(0, 0, 2, 1)となります。また、後半の8Byteをクリアする場合は、(2, 0, 4, 1)となります。 RGBA32UINTの64Byte(つまりRGBA32UINT x 4)のUAVの場合は、同じRECTでそれぞれ先頭32Byte、後半32Byteをクリアする事ができます。 また、クリアに使われる値ですが、R32UINTのリソースにはValue[4]の先頭の要素(Value[0])が繰り返し書き込まれます。RGBA32UINTには、Value[4]の要素が全て書き込まれます。 ほとんどのケースでゼロを書き込むと思いますが、思い通りの値でBufferを埋めたい場合はUAVのFormatとValue[4]に工夫が必要です。

一体いくつのRECTを指定可能なのでしょうか?

さて、今回これを書こうかなと思った直接の原因についてです。このAPIではRECTの数に上限が無いので、理論上はUINTの上限の個数までRECTが指定可能となっています。しかし実際は、クリア対象のリソースがそこまで大きいものを作成できないでしょう。 また、GPUへのコマンド構築時に何らかの形でRECTの情報を含めないといけませんが、UINTの上限はサイズ的に無理でしょう。今回私は256KByteのリソースにカウンタ記録用の16ByteのUAVを動的に確保するプログラムを作成し、 必要に応じてUAVの提供と、ゼロクリアを行うプログラムを記述しました。1フレームに2000個ほどのカウンタが確保されてクリアが行われました。クリア用にリソース全域を指定したUAVを使ってRECTを2000個設定したところ、 プログラムはこそでクラッシュしました。まぁ、2000は無理かと思いましたので、1023, 511, 255とコマンドを分割しましたがまだクラッシュします。127でクラッシュしなくなりました。私のシステムではこの値が上限値の様です。 さて、私が今書いているプログラムでは一体いくつを上限にするべきなのでしょうか。

shikihuiku
shikihuiku

リアルタイムレンダリングが好き