Work Graph in HLSL

基本的な機能に関する説明は割愛。自分が Work Graph を書くときに、度忘れしたときに参照するもののつもりで書いた。描画系はあとで書き足すかも。

HLSL関数のAttribute

  • [Shader("node")]
    • この属性をつけたHLSL関数は Work Graph のノードとして宣言。
  • [NodeIsProgramEntry]
    • Work Graph のエントリポイントとなれる。つまり、Input RecordをCommand Listや外部のGPUメモリから受け取ることができる。
  • [NodeLaunch("mode")]
    • “broadcasting”
      一つの Input Record を、複数の Dispatch Grid で共有して処理するノードとして宣言。
    • “coalescing”
      複数の Input Redcord を、一つの Dispatch Grid で処理するノードとして宣言。
    • “thread”
      一つの Input Record を、一つのスレッドで処理するノードとして宣言。
    • “mesh”
      “broadcasting”ノードと同様の起動方式だが、Work Graph の末端でしか使用できない。
      Mesh shaderとして動作する。(Amplification Shader は Work Graph ではサポートされていない)
  • [NumThreads(x,y,z)]
    • Thread Group のサイズ。通常の Compute Shader と同じ。
  • [NodeDispatchGrid(x,y,z)] or [NodeMaxDispatchGrid(x,y,z)]
    • NodeLaunch“broadcasting”の場合は、上記のいずれかが宣言されている必要がある。
    • NodeMaxDispatchGridが宣言されている場合は、Input Record 内のSV_DispatchGridセマンティクスの変数で Dispatch Grid のサイズが指定される必要がある。
      この場合は、Disapatch Grid サイズが Input Record ごとに変更できる。
  • [NodeID("nodeName",arrayIndex)]
    • ノードとしての識別名の定義。これを省略すると、関数名がノードの識別名になる。
    • 複数のHLSL関数で、Work Graph のノード配列を定義する場合は、同一の”nodeName”で、異なるarrayIndexを指定する。
      • arrayIndex は省略可能で、省略した場合は0番を宣言したとみなされる。
      • arrayIndex は、必ずしも連続して、隙間なく宣言されている必要はない。
    • ノード配列として定義されるノード要素に相当するHLSL関数は、以下の項目が同一でなければならない。
      • Input Record の宣言
      • NodeLaunch属性
      • NodeDispatchGrid属性、もしくはNodeMaxDispatchGrid属性で定義された Dispatch Grid のサイズ。
  • [NodeLocalRootArgumentsTableIndex(index)]
    • このノードが実行されるとき、indexで指定した Local Root Table がバインドされる。
    • この属性を定義しない場合や、indexに-1を設定した場合は、Work Graph がコンパイルされる時に自動で割り振られたインデックスがアサインされる。
  • [NodeShareInputOf("nodeIDWhoseInputToShare", optionalArrayIndex)]
    • 同一の Input Record で、異なる種類のノードを起動する場合は、同時に起動するノードには、この属性で、Input Record を共有するノードが示されている必要がある。
    • 異なる種類のノードが Input Record を共有する場合は、一つの代表するNodeIDを他のすべてのノードが指すように宣言する。
    • 最高で256種類のノードが同一の Input Record で起動できる。
    • RW Input Record(書き込み可能な Input Record)は、異なる種類のノードで共有することはできない。
  • [NodeMaxRecursionDepth(count)]
    • このノードの最大再帰呼び出し回数を宣言する。
    • 参考:現在のWork Graphでは複数ノードを介した再帰呼び出しグラフはサポートされていない。
  • [NodeMaxInputRecordsPerGraphEntryRecord(count, sharedAcrossNodeArray)]
    • この”mesh"ノードが一度に受け取ることのできる Input Record の最大数を宣言する。
    • この最大数は、DispatchGrid()呼び出し時の、一つの Input Record によって動作する全ての Work Graph ノードが出力する、このノードに対する Input Record の総数という意味。
    • sharedAcrossNodeArraytrueの場合は、Input Record を受け取るノード配列全体で、この最大数を共有する。
    • 参考:GPUのアーキテクチャによっては、コンピュートシェーダーを実行するときと、描画用のシェーダーを実行するときに、実行コンテキストのスイッチが必要なものがある。
      頻繁な実行コンテキストスイッチを避けるため、”mesh”ノードへの Input Record は可能な限り蓄積される必要があるが、その上限を定義することで、Backing Memory のサイズと実行性能を適切にバランスすることができるようになると思われる。

Input Record

Input Recordは、自身のNodeLaunch属性で受け取れる型が決まる。

Broadcasting Launch

  • DispatchNodeInputRecord<recordType>
    • 読み出し専用の Input Record。
  • RWDispatchNodeInputRecord<recordType>
    • 読み書きが可能な Input Record。一時的なUAVバッファのように扱える。
  • globallycoherent RWDispatchNodeInputRecord<recordType>
    • 前者と基本的に同じだが、Barrier()FinishedCrossGroupSharing()メソッドを適切に使うことで、
      Input Record のメモリ領域を、同時に起動した Dispatch Grid の他の Thread Group とのコミュニケーションにつかうことができる。
    • 参考:FinishedCrossGroupSharing()メソッドを使うときは、Input Recordの構造体に、[NodeTrackRWInputSharing]属性をつけなくてはいけない。

Coalescing Launch

  • [MaxRecords(maxCount)] GroupNodeInputRecords<recordType>
    • 1 ~ maxCount数の、読み出し専用の Input Record。
    • Count()メソッドで、受け渡された Input Record の配列の長さがわかる。
  • [MaxRecords(maxCount)] RWGroupNodeInputRecords<recordType>
    • 読み書き可能なInput Record。一時的なUAVバッファのように扱える。
    • Count()メソッドで、受け渡された Input Record の配列の長さがわかる。
    • 参考:この Input Record にアクセスできるのは、Coalescing Launch の特性上、一つの Thread Group なので、あまり使い道がないかもしれない。
  • [MaxRecords(maxCount)] EmptyNodeInput
    • データの受け渡しはない。
    • Count()メソッドで、受け渡された Input Record の配列の長さがわかる。

Thread Launch

  • ThreadNodeInputRecord<recordType>
    • 読み出し専用の Input Record。
  • RWThreadNodeInputRecord<recordType>
    • 読み書きが可能な Input Record。一時的なUAVバッファのように扱える。
    • ただし、Thread Launch なので、他のスレッドとのデータのやり取りなどには使えない。

Node OutputとOutput Record

Input Record は、ノード関数の引数で直接受け取る形になっているが、Output Record は、NodeOutput 型が Output Record を抽象化して保持している形になっているので注意。 Input Record とは異なり、呼び出し先のノードの NodeLaunch属性にかかわらず、同じ型を使用する。

Node Output

配列型かどうかと、Empty型かどうかを選択する形で、計4種類がある。

  • attribute-list NodeOutput<recordType>
    • 単一の Node Output を出力する場合。
    • 注意:Output Record 自体が、基本的には可変長の配列のようなものなので、Output Record が一つしか出力できないという意味ではない。
  • attribute-list NodeOutputArray<recordType>
    • 配列型の Node Output を出力する場合。
    • この出力を受け取るノードは、配列で宣言されていることが期待される。
    • operator [] で、上記のNodeOutput型が取得できる。
  • attribute-list EmptyNodeOutput
    • Output Record を出力しない場合の Node Output。
  • attribute-list EmptyNodeOutputArray
    • 配列型のEmptyNodeOutput
    • operator [] で、上記のEmptyNodeOutput型が取得できる。

上記の attribute-list について

  • [MaxRecords(count)] or [MaxRecordsSharedWith(nameInShader)]
    • countで、出力する Output Record の最大数を宣言する。
    • MaxRecordsSharedWithは、このノードの関数の引数宣言で、先に宣言されたnameInShader引数と同じ最大数を、宣言として使う場合に使用する。
      • 同じ最大数を、複数の下流ノードに出力する場合に有用。
    • 配列型の Node Output は、配列のすべての要素に対してこの最大数が適用される。
  • [NodeID("nodeName")] or [NodeID("nodeName",baseArrayIndex)]
    • 出力先のノード名を明示的に宣言する。
    • この属性をつけない場合は、宣言した変数名が出力先のノードとみなされる。
  • [AllowSparseNodes]
    • 出力先のノードが存在しないことを許可する。
    • 特に配列型の Node Output では、いくつかの配列要素に対する出力ノードが存在しないことを許可する。
      • 参考:IsValid()メソッドで、Work Graph に有効な出力ノードが存在するかを確認することができる。
  • [NodeArraySize(count)] or [UnboundedSparseNodes]
    • countで、配列型の Node Output の最大要素数を宣言する。
    • UnboundedSparseNodesは、[NodeArraySize(0xffffffff)] [AllowSparseNodes]と宣言するのと同義。
      • 参考:実際には、Work Graphをコンパイルするときに、存在する有効な出力ノードの数と範囲は検出されるので、このサイズの Output Record が作られるわけではない。

Output Record の生成と取得、操作など。

基本的には、Output Record を、スレッド単位で確保するか、Therad Group 単位で確保するかで分かれている。
各メソッドの呼び出し時には、Thread Group が Uniform でなくてはならないというルールがある。
(つまり、すべてのスレッドがその命令を通過するか、すべてのスレッドがその命令を通過しないかのいずれかでなくてはならない。)

  • ThreadNodeOutputRecords<recordType> NodeOutput<recordType>::GetThreadNodeOutputRecords(uint numRecordsForThisThread)
    • Output Record を個々のスレッド単位で確保する。
    • 呼び出しは Unfirom だが、確保する Output Record の数や、配列型 Output Record の場合の、確保する要素のインデックスは、各スレッドで可変でよい。
    • Output Record のハンドルの配列が返される。Get(int index=0)メソッドが、Output Record の配列の先頭に対する簡単なアクセスを提供する。
    • すべての書き込み処理が終了したら、呼び出しが Uniform な状態で、取得した ThreadNodeOutputRecords<> に対してOutputComplete()メソッドを必ず呼び出さなければならない。
  • GroupNodeOutputRecords<recordType> NodeOutput<recordType>.GetGroupNodeOutputRecords(uint numRecords)
    • Output Record を Thread Group 全体でnumRecord数確保する。
    • 呼び出しは Unfirom で、確保数や、配列型 Output Record の場合の確保する要素のインデックスも Uniform でなくてはならない。
    • すべての書き込み処理が終了したら、呼び出しが Uniform な状態で、取得したGroupNodeOutputRecords<>に対してOutputComplete()メソッドを必ず呼び出さなければならない。
    • Thread Launch のノードではこのメソッドを呼び出すことはできない。
  • EmptyNodeOutput::ThreadIncrementOutputCount(uint count)
    • スレッド単位で(Emptyな)Output Record の数をインクリメントする。
    • 呼び出しは Uniform である必要があるが、個々のスレッドが指定するインクリメント数は可変でよい。
  • EmptyNodeOutput::GroupIncrementOutputCount(uint count)
    • Thread Group 単位で(Emptyな)Output Recordをcount数インクリメントする。
    • 呼び出しは Uniform で、インクリメント数も Uniform でなければならない。

Barrier()組み込み関数

Barrier()組み込み関数は、Work Graphの導入に伴いShader Model 6.8から新しく導入されたが、Work Graphに関係なく使える。
以下の3つの型がある。

  • void Barrier(uint MemoryTypeFlags, uint SemanticFlags)
    • MemoryTypeFlagsで指定したタイプの、すべてのリソースの同期をとる。
    • MemoryTypeFlags は ビットマスクになっていて下記を組み合わせて指定できる。
      • UAV_MEMORY
      • GROUP_SHARED_MEMORY
      • NODE_INPUT_MEMORY
      • NODE_OUTPUT_MEMORY
      • ALL_MEMORY
        • これはすべてのビットマスクの組み合わせ。
  • template<typename UAVResource> void Barrier(UAVResource, uint SemanticFlags)
    • 引数で指定したUAVResourceを同期の対象とする。
  • template<typename NodeRecordObject> void Barrier(NodeRecordObject o, uint SemanticFlags)
    • 引数で指定したNodeRecordObjectの同期をとる。
    • NodeRecordObjectは、RW{Dispatch|Group|Thread}NodeInputRecords or {Group|Thread}NodeOutputRecordsを指定できる。

SemanticFlagsについて

どのような同期をとるかを指定するのがSemanticFlags。以下の3つがある。_SYNC_SCOPEは論理和で組み合わせて指定できる。

  • GROUP_SYNC

    • Thread Group 内のすべてのスレッドが、このバリア命令の直前の命令まで発行し終わるまで、ここですべてのスレッドが待つことを指示する。
    • 他の2つはメモリアクセスの完了に関連する制御だが、これはシェーダーの命令発行の制御。
  • GROUP_SCOPE

    • このバリア命令の前に発行された、メモリアクセス処理が完了するまで、ここで待つことを指示する。
    • 参考:GROUP_SYNCと組み合わせて指定した場合は、Thread Group 内のすべてのスレッドが、
      このバリア命令の前に記述されたすべてのメモリアクセス処理が、完了するまでここで待つことを指示する。
    • 参考:GROUP_SYNCがない場合は、Thread Group が複数の Wave に分かれている場合は、Wave 間での同期は保証されない。
    • 例として、以下のオブジェクトに対してのメモリアクセスを同期したい場合に使う。
      • groupsharedで修飾されたオブジェクト
      • RWGroupNodeInputRecords
      • GroupNodeOutputRecords
  • DEVICE_SCOPE

    • このバリア命令の前に発行された、メモリアクセス処理が完了するまで、ここで待つことを指示する。
      加えて、globallycoherentで修飾されたオブジェクトに対するメモリアクセス処理と、Interlocked系の命令によるメモリアクセス処理が完了して、
      GPUの他の処理ユニットからその処理結果が正しく読み出せるようになるまで、ここで待つことを指示する。
    • 参考:GROUP_SYNCと組み合わせた場合の効果は、GROUP_SCOPEの項で説明したのと同じ。
    • 具体的には、以下のオブジェクトに対してのメモリアクセスを同期したい場合に使う。
      • globallycoherentで修飾されたUAV
      • globallycoherentで修飾されたRWDispatchNodeInputRecord

Work Graphにおけるgloballycoherent修飾について。

  • globallycoherent修飾されたUAVについて

    • Work Graph の上流ノードと下流ノードでのデータの受け渡しは、基本的に{Input|Output} Record を使うが、データの格納形式や、必要とされるデータの寿命などによって、UAVを使うのが適切な場合がある。
    • globallycoherent修飾されたUAVを介して、上流ノードと下流ノードでデータを受け渡す場合は、上流ノードにおいて、UAVに対する処理と、下流ノードを起動するための Output Record の生成完了(つまり、OutputComplete()の呼び出しや、IncrementOutputCount()の呼び出し)の間に、DEVICE_SCOPEのバリアを設定することで、両ノードの、UAVへのデータ競合を避けることができる。
      • 注意:Work Graphの実行順序は、必ずしもグラフの上流ノードから下流ノードと順番が決まっているわけではなく、{Input|Output} Recordの依存関係で決まっている。
      • 上流ノードの、複数の Thread Group が同一のUAVを処理し、全ての処理の完了を待ってから下流ノードの処理を始めたい場合は、InterlockedAdd()などを駆使して、上流ノードの Thread Group の中で、最後に処理が完了した Thread Groupを検出して、その Thread Group が下流ノードを起動するための Output Record を生成する必要がある。
  • globallycoherentで修飾されたRWDispatchNodeInputRecordについて

    • Broadcasting Launch では、複数のThread Group が、一つの Input Recordを読み書きすることができる。
      globallycoherent修飾をつけておけば、globallycoherent修飾のついた短期的なUAVバッファとして扱うことができるので、 Interlocked系の命令やバリアを適切に使用すれば Thread Group 間で、データのやり取りおよび同期をとることができる。

    • bool RWDispatchNodeInputRecord<recordType>::FinishedCrossGroupSharing()を使えば、最後にこのメソッドを呼び出した Thread Group にのみ、trueが返されるので、複数の Therad Group で行った処理全体の終了を知ることができる。

      • このメソッドを使用するInput Recordの宣言には、[NodeTrackRWInputSharing]属性がついてる必要がある。この属性をつけると、Input Recordのサイズが4byte大きくなる。
        • 参考:結局のところ、この4byteの領域を上流ノードでゼロに設定しておき、各Therad GroupがRWDispatchNodeInputRecordに対する処理が終了した時点で、その4byte領域にInterlockedAdd()を実行していると思われる。

RWThreadNodeInputRecord, ThreadNodeOutputRecordsオブジェクトとBarrier()について

  • この二つのオブジェクトは、いずれもスレッドローカルなので、GROUP_SCOPE, DEVICE_SCOPEいずれも適用できない。
  • Barrier()命令を跨いで、対象オブジェクトに対するメモリアクセス命令を、コンパイラによる命令スケジューリングで移動することを抑制する。
shikihuiku
shikihuiku

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

Related