HLSL ByteAddressBuffer の使い方

ByteAddressBuffer は引数にバイト数を指定する。

ByteAddressBuffer byteBuffer : register(t0);
uint value = byteBuffer.Load(0);

4 Byte 単位でメモリーにアクセスしてデータを取得出来るバッファータイプ。戻り値は uint (4 Byte) になる。Load 関数の引数は 4 の倍数でなければいけない。Load2(0) で指定すれば uint2 型で 8 Byte の連続した領域を取得できる。Load3(0) で指定すれば uint3 型で 12 Byte の連続したメモリー。

uint4 value = byteBuffer.Load4(4);
という指定をすれば、アドレスの開始位置が先頭から 4 Byte 目から、16 Byte の連続した領域を取得出来る。

DirectX12 HLSL Texture2D.Load にはまる

Texture2D の Load 命令は引数として int3 を受け取るため、texture.Load(int3(0, 0, 0)); のような形式で呼び出す。この時 x, y には UV ではなくて テクスチャの width -1, height -1 の値を指定する。当たり前だが、-1 に注意。そして z は mipmap を指定できる。

Texture サイズが 256×256 だとして、Load(int3(256, 256, 0)); とサイズを超えてしまった場合は 0 が返るようになっている。この時に大切なのが存在しない mipmap にアクセスした場合も 0 が返るという事。

int3 を指定せずに Load(0) とやるのは特に問題無いが、テストで Load(1) としてしまったとき、Load(int3(1, 1, 1)); になるため、もし mipmap が存在しないテクスチャに対してこれをした場合は 0 が返ってしまう。ちょっとしたテストで Load(1) をやった結果、原因を突き止めるのに時間がかかった。

Texture2D.Load は int3 でテクスチャ座標とミップマップ値に気をつけて使用する。

DirectX12 Misc

ID3D12Resource::GetGPUVirtualAddress

D3DResource は GetGPUVirtualAddress メソッドを持っており、Vertex Buffer を作るときなど View 構造体に対してアドレスを渡すときに使う。このメソッドは Buffer Resource のみで使用可能で、Texture Resource の時は使用できないため 0 が返る。

Root Parameter の並び順

アクセス頻度の高いものをインデックス 0 から並べた方が、パフォーマンスが良い? (未検証)

DirectX12 Root Signature

Root Signature を覚えるのは大変だ。これは抽象的な図や分かりやすい言葉で表すのは困難で (少なくとも自分には)、C言語を最初に学んだ時のポインタのように、苦しんで覚えるしかない類の物だと思ってる。

MS のドキュメントやこんなブログの内容を見るよりも、D3D12 のサンプルプログラムを実行し、修正しながら動作を確認するのが一番の近道な気がします。

自分は Root Signature を理解するのに最も大切な概念って Descriptor Table だと勝手に思い込んでいたのですが (いや重要なんですけどね…)、CD3DX12_ROOT_PARAMETER1 が大切で、この定義と、CommandList->SetGraphicsRootDescriptorTable() との関連性が理解出来れば、Root Signature って大体分かるんだと思う。

DirectX12 Compute Shader

Compute Shader の実行は D3D12ExecuteIndirect の Sample を参考に。

Graphics Task 用と Compute Task 用で Command Allocator と Command List の分割。D3D12_COMMAND_LIST_TYPE_COMPUTE を指定して作成。

Root Signature

Graphics と Compute で使うリソースが異なるため、当然 Root Signature も分けて作成される。

Resource Binding

このサンプルの Compute Shader で使われるリソースは b0, t0, t1, u0 の4つ。

cbuffer RootConstants : register(b0) {…}
StructuredBuffer cbv : register(t0);
StructuredBuffer inputCommands : register(t1);
AppendStructuredBuffer outputCommands : register(u0);

HLSL Structured Buffer へのアクセス

Shader から Structured Buffer にアクセスする場合は、

StructuredBuffer Buffer0 : register(t0);

このように Shader 上で定義を行い Buffer0[0] や Buffer0[16] のように Array of Structure の形でアクセス出来る。注意が必要なのが、データ定義時は[] でのサイズ指定は必要ないということ。

仮にもし [] でサイズ指定をしてしまうと、それは複数のリソースを連続定義したことになる。

StructuredBuffer Buffer0[10] : register(t0);

この指定は t0 – t9 まで、複数の SRV を使うという指定になる。

アクセスする場合は Buffer0[2][16] という形になる。

Compute Shader の Dispatch と Thread Group

Compute Shader で実行される Thread 数は、CommandList::Dispatch(x, y, z) と Shader 側で指定する numthreads[x, y, z] の数によって決まる。

CommandList::Dispatch(5, 3, 2) , numthreads[10,8,2] で実行した場合は、30 の Thread Group 内でそれぞれ、160 threads ずつ動作する。

numthreads

Thread Group 内のスレッド数を指定する。Dispatch(1, 1, 1) で起動した場合は、Thread Group の個数は1になる。numthreads に指定した X * Y * Z のスレッド数が、Thread Group 内で起動する。

4×4 の matrix にアクセスする場合は numthreads[4, 4, 1] のように指定して、SV_GroupThreadID のxy値を取得することで 4×4 に簡単にアクセスできる。

numthreads の最大数

Shader Model 5.0 以降では、Thread Group ごとに起動する numthreads の最大数は 1024 まで。x * y * z が 1024 を超えてはいけない。

numthreads(16, 16, 4) とか numthreads(32, 32, 1) とか numthreads(8, 8, 16) とか。

システム値

uint GI : SV_GroupIndex
uint3 DTid : SV_DispatchThreadID
uint3 GTid : SV_GroupThreadID
uint3 Gid : SV_GroupID

上記4つが、エントリーポイント関数から取得出来る。

SV_GroupID (uint3)

Thread Group のインデックス値が返る。Dispatch(2,1,1) で呼び出した場合 2つのスレッドグループが起動して、その値は (0,0,0) または (1,0,0) になる。

SV_GroupThreadID (uint3)

Thread Group 内の個々のスレッドのインデックス値が返る。numthreads[3,2,1] で起動した場合は、このスレッドグループは6つのスレッドが起動して、その値は[0-2,0-1,0] になる。

SV_GroupIndex (uint)

Thread Group 内のインデックス値である SV_GroupThreadID を 1D 化したインデックスが返る。

SV_GroupIndex = SV_GroupThreadID.z * dimx * dimy + SV_GroupThreadID.y * dimx + SV_GroupThreadID.x

Thread Group の数 Dispatch(x,y,z) には依存しない。numthreads で指定した値に依存。そのため、このシステム値が返す最大は 0-1023 まで。

※ dim は numthreads で指定した次元。

SV_DispatchThreadID (uint3)

Thread と Thread Group とを組み合わせたインデックス値。計算方法はSV_GroupID (Dispatch された現在の Thread Group のインデックス) * numthreads で指定されたxyz + GroupThreadID (Thread Group 内の現在のインデックス値)

Dispatch(2,2,2) で numthreads[3,3,3] の場合は範囲は各 0…5 が返る。
Dispatch(5,3,2) で numthreads[10,8,3] の場合は範囲は [0,0,0] から [49,23,5] までが返る。

DispatchThreadID で 640×480 px を処理

Dispach(32, 24,1) で numthreads[20, 20, 1] で実行すれば、SV_DispatchThreadID の値は [0,0,0]-[639,479,1] の範囲で返ってくる。

int3 location = int3(0,0,0);
location = DispatchThreadID.x;
location = DispatchThreadID.y;
float value = InputTex.Load(location);

でアクセスすることが可能。
(Reference: Practical Rendering & Computation with Direct3D11)

参考リンク

Compute Shader Overview
https://docs.microsoft.com/ja-jp/windows/win32/direct3d11/direct3d-11-advanced-stages-compute-shader

ID3D11DeviceContext::Dispatch method
https://docs.microsoft.com/ja-jp/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-dispatch

numthreads
https://docs.microsoft.com/ja-jp/windows/win32/direct3dhlsl/sm5-attributes-numthreads

この記事とは関係無いですが、DirectX とか OpenGL は過去バージョンのドキュメントにしか無い情報が沢山あり、それが新しく始める場合の障壁になる。

HLSL の Texture Gather 命令

HLSL の Texture Gather 命令を使用すると 1px の 4 要素ではなく、4px の 1要素が取得出来る。Gather は xyzw には指定した uv (左上原点) からのオフセット x[0,1] y[1,1] z[1,0] w[0,0] の値が入る。

下記のブログが非常に分かりやすい記述がありました。素晴らしい記事に感謝です。

DirectX 11, HLSL, GatherRed
http://wojtsterna.blogspot.com/2018/02/directx-11-hlsl-gatherred.html

DirectX12 の SRV と UAV の違い

SRV と UAV の使い方のお作法がたまに分からなくなる。使っていないと分からなくなりますね (ただの言い訳で初めからきちんと理解していない)

元々は Structured Buffer って Shader Resource View として register(#t) にバインド出来るんだっけ?という事が分からなかった。結論から言えば、Structured Buffer は #t でバインドして読み込まないといけない。

それと Structured Buffer のようなデータを #t で使わずに、同じようなデータを #b にして良いのでは?という思いもある。別に UAV 使わなければ CBV で良い。

UAV から SRV へ

あるバッファを SRV として作成する場合は、D3D12_RESOURCE_DESC の Flags には特に何も指定せず D3D12_RESOURCE_FLAG_NONE で作成する。これで HLSL のリソースとして register(#t) にバインド出来る。

UAV を作成する場合は、Flags に D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS を指定する。ただ UAV Flag をつけると、メモリレイアウトの問題で付けていない場合と比べてアクセス速度が低下するらしい (MS doc)。

ある Structured Bufferを Compute Shader で書き込み、それを Pixel Shader で読み込みたい場合などは RWStructuredBuffer : register(#u) で CS 上で書き込みを行い、読み込む場合は (RW無しの) StructuredBuffer : register(#t) で読み込める。

RW 系バッファと UAV

RWTexture2D や RWStructuredBuffer など、RW 系バッファは UAV として register(#u) にバインドする。これは、Pixel Shader でも Compute Shader でも一緒。読み込みを行わない Read Only の場合は SRV として register(#t) にバインドする。

Byte Address Buffer

Raw Buffer を直接扱えるバッファータイプ。生のメモリーに直接指定してアクセスって今はそうそう機会が無く、殆どの場合 StructuredBuffer でも代用できるのでそちらを使う事が多かったりする。

Buffer と ByteAddressBuffer の違い

って何だ? Buffer と ByteAdressBuffer は SRV として register(#t) でアクセス出来、RWBuffer と RWByteAddressBuffer は UAV として register(#u) でアクセス出来る。

ByteAddressBuffer は Byte のオフセットでメモリーにアクセス出来るが 4 バイト単位が必須。(Buffer の場合はインデックス値でアクセス) 返ってくるのは uint 型のみなので、自分で変換をかける必要がある。

ByteAddressBuffer には、Load4 メソッドがあるが、これは指定したアドレスから、uint 型4つ (16byte) ロードされるという認識で良いのかな。

Buffer の場合は宣言時に型指定が必要。データアクセスはインデックス値。int, float 単位でデータアクセスする場合は、Buffer で良い。

https://www.gamedev.net/forums/topic/678018-rwbuffer-vs-rwstructuredbuffer-or-rwbyteaddressbuffer/