Heap管理をまとめて、ReadbackのHeapも用意できるようになったので使えるようにしてみる。
Heapの種類
Heapの種類にはUpload、Default、Readback、Customと4つ定義されている。
Customは使うつもりがないので対象は3つ。
CPUのメモリと各種Heapの関係はこんな感じ。
それぞれのHeapに利用用途に合わせて各種Viewを付けて、GPUから参照する。
Upload Heap
Uploadは書き込み専用のポインタをMapして、CPU側からデータを直接書き込める。ただし遅いので常時使うのは小さな定数バッファぐらい。また、Default HeapはCPU側から直接扱えないので、データをコピーするために一時的にUploadを経由させるために使う。
Default Heap
GPUが一番効率よくメモリを使える場所。
CPU側からは直接参照できない。
Readback Heap
Readbackは読込専用のポインタをMapして、CPU側からデータを直接参照できる。
Default HeapをReadback経由で参照する場合、まずデータをコピーする。
Uploadでデータをコピーする際、コピー先のリソースのステータスはD3D12_RESOURCE_STATE_COPY_DESTだったがReadbackでデータをコピーする際は、コピー元のリソースのステータスはD3D12_RESOURCE_STATE_COPY_SOURCEにする必要がある。
コピーの完了を待って、Mapで読み取り用のポインタを取得する。
このときD3D12_RANGEを渡すが、Uploadの場合0,0で初期化していた。これはCPU側から読み取らない宣言だったわけだけど、Readbackの場合は読み取るので、読み取るオフセットを指定する。
UAV経由でデータを書き換える
GPUでデータを書き換える代表格はレンダーターゲットや深度バッファになるけど、Compute Shaderで計算結果を書き込むのはUnordered Access Viewを使うことになる。その時のデータ型にもいろいろ種類があるが、ここでは2つで例を挙げる。
RWStructuredBuffer
StructuredBufferの書き込める版。
データソースとしてStructuredBufferで渡されたデータをCompute Shaderで、計算した結果を同じインデックスのRWStructuredBufferに書き込むといったような使い方をする。
AppendStructuredBuffer
Appendはキューのようなデータ構造で、スレッドの処理が完了した順に追加していく。例えばカリングを行って対象データのみ追加する場合、処理後のデータ数がデータ元と異なる。
その変わったサイズを管理するのがUAV Counter。そのためAppendを使う場合UAVにはカウンタが必須になる。
UAV Counterとは
以前UAVのことについて書いたが、このときはUAVカウンタのことを理解していなかった。
このカウンタはUAVにオプションで付けることが出来て、RWStructuredBufferの場合、IncrementCounter、DecrementCounter関数、AppendStructuredBufferの場合、Append関数を使うとカウントが制御される。
バッファに入っているデータ数を管理するカウンタとして利用できるということがわかった。
そのため、データを書き込む前には一旦0にリセットする必要がある。
(ExecuteIndirectを使う場合、Readback不要でシェーダで処理したUAVリソースとカウンタのオフセットをそのまま渡すようなI/Fになっている)
#define DefRS "RootFlags(DENY_VERTEX_SHADER_ROOT_ACCESS|DENY_HULL_SHADER_ROOT_ACCESS|DENY_DOMAIN_SHADER_ROOT_ACCESS|DENY_GEOMETRY_SHADER_ROOT_ACCESS|DENY_PIXEL_SHADER_ROOT_ACCESS),DescriptorTable( SRV(t0, flags=DATA_STATIC_WHILE_SET_AT_EXECUTE),visibility=SHADER_VISIBILITY_ALL),DescriptorTable( UAV(u0, flags=DATA_VOLATILE),visibility=SHADER_VISIBILITY_ALL)"
StructuredBuffer<uint> Src : register(t0) ;
AppendStructuredBuffer<uint> Dst : register(u0) ;
struct CSInput
{
uint3 ID : SV_DispatchThreadID ;
} ;
[RootSignature(DefRS)]
[numthreads( 8, 1, 1 )]
void CSMain( CSInput In )
{
int i = In.ID.x ;
if( Src[i] > 30 ) {
Dst.Append( Src[i]) ;
}
return ;
}
試しに作ったサンプルでは、Srcで渡した点数データをDstにAppendしていく。
その際、30点以下の場合は除外するようにした。
100、80、60、50、40、30、20、10の8個のデータをSrcで渡すと
Dstで100、80、60、50、40の5個のデータと、カウンタには5という値が渡る。
![]() |
| Src |
![]() |
| Dst |
![]() |
| Counter |
Readback経由でCounterを取得して、その数分Dstの先頭から値を取得するとGPUで処理した結果が受け取れるようになった。
これでGPUの処理結果を受け取れるようになったので、単純で大量の計算をGPUに任せることも出来る。
今回のハマりポイント
データ元を構造化バッファにしていたけど、最初は定数バッファで試していた。
定数バッファの場合、全体を配列で使えないので構造体内に配列を定義した。
struct _Src
uint Val[8] ;
} ;
ConstantBuffer<_Src> Src : register(b0) ;
AppendStructuredBuffer<uint> Dst : register(u0) ;
struct CSInput
{
uint3 ID : SV_DispatchThreadID ;
} ;
[RootSignature(DefRS)]
[numthreads( 8, 1, 1 )]
void CSMain( CSInput In )
{
int i = In.ID.x ;
if( Src.Val[i] > 30 ) {
Dst.Append( Src.Val[i]) ;
}
return ;
}
C++側からは、バイナリで4バイトずつ8つ(32バイト)のデータを書き込んだ。
PIXで確認してもきちんとデータが入っている。
にも関わらず出力結果は構造化バッファと同じにならず、2件、100と40が返却された。
DXILの_Src構造体のサイズ情報が116となっており、32バイトではない。
どうやら配列の1要素単位で16バイトアライメントになっているみたいで、最後の項目だけ4バイトと考えるとサイズが一致する。
試しに渡すバイナリを16バイト単位にすると、同じ結果が得られた。
PIX上のデータには0、4に値が入っていて、1,2,3,5,6,7には値が入っていないように見えるが、シェーダは結果は正しい値になっている。
BufferFormatが実際のデータと合っていないっぽい。
struct _Src
uint4 Val[2] ;
} ;
ConstantBuffer<_Src> Src : register(b0) ;
AppendStructuredBuffer<uint> Dst : register(u0) ;
struct CSInput
{
uint3 ID : SV_DispatchThreadID ;
} ;
[RootSignature(DefRS)]
[numthreads( 8, 1, 1 )]
void CSMain( CSInput In )
{
int i = In.ID.x / 4 ;
int j = In.ID.x % 4 ;
if( Src.Val[i][j] > 30 ) {
Dst.Append( Src.Val[i][j]) ;
}
return ;
}
定義をuint4に変更して、渡すデータも4バイト単位に戻して実行したらうまく行った。
バイナリデータとアライメントが間違っていただけだった。
配列で定義すると、1要素単位で16バイトアライメントになる。
配列にせず、uintを8つ変数定義すると期待通り4バイトアライメントになる。floatなども同じ。




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