D3D12のRoot Signature 1.1について

前提知識として、D3D12の基本的なResource Bindingが理解できているものとして進めます。
Windows 10 Anniversary Update(build 14393 または Version 1607)より、RootSignature1.1が導入されました。引き続きRootSignature1.0が使用可能ではありますが、RootSignature1.1に変更することによってどんなメリットがあるのか見ていきたいと思います。

まずはRootSignature1.0と1.1の違いを見てみます。

RootSignature1.0

typedef struct D3D12_ROOT_SIGNATURE_DESC {
  UINT                            NumParameters;
  const D3D12_ROOT_PARAMETER      *pParameters;
  UINT                            NumStaticSamplers;
  const D3D12_STATIC_SAMPLER_DESC *pStaticSamplers;
  D3D12_ROOT_SIGNATURE_FLAGS      Flags;
} D3D12_ROOT_SIGNATURE_DESC;

RootSignature1.1

typedef struct D3D12_ROOT_SIGNATURE_DESC1 {
  UINT NumParameters;
  const D3D12_ROOT_PARAMETER1 *pParameters;
  UINT NumStaticSamplers;
  const D3D12_STATIC_SAMPLER_DESC *pStaticSamplers;
  D3D12_ROOT_SIGNATURE_FLAGS Flags;
} D3D12_ROOT_SIGNATURE_DESC1;

違いがあるのは、D3D12_ROOT_PARAMETER(1)のみで、ほかは同じです。
続けて、D3D12_ROOT_PARAMETER(1)を見てみます。

RootSignature1.0

typedef struct D3D12_ROOT_PARAMETER {
  D3D12_ROOT_PARAMETER_TYPE ParameterType;
  union {
  D3D12_ROOT_DESCRIPTOR_TABLE DescriptorTable;
  D3D12_ROOT_CONSTANTS Constants;
  D3D12_ROOT_DESCRIPTOR Descriptor;
  };
  D3D12_SHADER_VISIBILITY ShaderVisibility;
} D3D12_ROOT_PARAMETER;

RootSignature1.1

typedef struct D3D12_ROOT_PARAMETER1 {
  D3D12_ROOT_PARAMETER_TYPE ParameterType;
  union {
  D3D12_ROOT_DESCRIPTOR_TABLE1 DescriptorTable;
  D3D12_ROOT_CONSTANTS Constants;
  D3D12_ROOT_DESCRIPTOR1 Descriptor;
  };
  D3D12_SHADER_VISIBILITY ShaderVisibility;
} D3D12_ROOT_PARAMETER1;

違いは、D3D12_ROOT_DESCRIPTOR_TABLE(1)と、D3D12_ROOT_DESCRIPTOR(1)にあります。
まず、D3D12_ROOT_DESCRIPTOR_TABLE(1)を見てみます。

RootSignature1.0

typedef struct D3D12_ROOT_DESCRIPTOR_TABLE {
  UINT NumDescriptorRanges;
  const D3D12_DESCRIPTOR_RANGE *pDescriptorRanges;
} D3D12_ROOT_DESCRIPTOR_TABLE;

RootSignature1.1

typedef struct D3D12_ROOT_DESCRIPTOR_TABLE1 {
  UINT NumDescriptorRanges;
  const D3D12_DESCRIPTOR_RANGE1 *pDescriptorRanges;
} D3D12_ROOT_DESCRIPTOR_TABLE1;

こちらは、D3D12_DESCRIPTOR_RANGE(1)に変更がありますがそれ以外は同一です。
下記の通り、D3D12_DESCRIPTOR_RANGE(1)を見てみると、D3D12_DESCRIPTOR_RANGE_FLAGSが新たに追加されています。

RootSignature1.0

typedef struct D3D12_DESCRIPTOR_RANGE {
  D3D12_DESCRIPTOR_RANGE_TYPE RangeType;
  UINT NumDescriptors;
  UINT BaseShaderRegister;
  UINT RegisterSpace;
  UINT OffsetInDescriptorsFromTableStart;
} D3D12_DESCRIPTOR_RANGE;

RootSignature1.1

typedef struct D3D12_DESCRIPTOR_RANGE1 {
  D3D12_DESCRIPTOR_RANGE_TYPE RangeType;
  UINT NumDescriptors;
  UINT BaseShaderRegister;
  UINT RegisterSpace;
  D3D12_DESCRIPTOR_RANGE_FLAGS Flags;
  UINT OffsetInDescriptorsFromTableStart;
} D3D12_DESCRIPTOR_RANGE1;

一方で、D3D12_ROOT_DESCRIPTOR_TABLE(1)を見てみると、こちらも、D3D12_ROOT_DESCRIPTOR_FLAGSが新たに追加されいます。

RootSignature1.0

typedef struct D3D12_ROOT_DESCRIPTOR {
  UINT ShaderRegister;
  UINT RegisterSpace;
} D3D12_ROOT_DESCRIPTOR;

RootSignature1.1

typedef struct D3D12_ROOT_DESCRIPTOR1 {
  UINT ShaderRegister;
  UINT RegisterSpace;
  D3D12_ROOT_DESCRIPTOR_FLAGS Flags;
} D3D12_ROOT_DESCRIPTOR1;

つまり両者の違いは、

  • Descriptorに対してD3D12_ROOT_DESCRIPTOR_FLAGSの追加
  • DescriptorTableに対してD3D12_DESCRIPTOR_RANGE_FLAGSの追加

のみで、ResourceのBinding方法などについては大きな変更はありません。
RootSignatureを生成する際も、D3D12SerializeRootSignature()に上記構造体と共に、D3D_ROOT_SIGNATURE_VERSION_1_1を引数で渡すだけで、特に大きな変更はありません。

そして以下が、新設されたフラグになります。

typedef enum D3D12_DESCRIPTOR_RANGE_FLAGS { 
  D3D12_DESCRIPTOR_RANGE_FLAG_NONE = 0,
  D3D12_DESCRIPTOR_RANGE_FLAG_DESCRIPTORS_VOLATILE = 0x1,
  D3D12_DESCRIPTOR_RANGE_FLAG_DATA_VOLATILE = 0x2,
  D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE = 0x4,
  D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC = 0x8
} D3D12_DESCRIPTOR_RANGE_FLAGS;
typedef enum D3D12_ROOT_DESCRIPTOR_FLAGS { 
  D3D12_ROOT_DESCRIPTOR_FLAG_NONE = 0,
  D3D12_ROOT_DESCRIPTOR_FLAG_DATA_VOLATILE = 0x2,
  D3D12_ROOT_DESCRIPTOR_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE = 0x4,
  D3D12_ROOT_DESCRIPTOR_FLAG_DATA_STATIC = 0x8
} D3D12_ROOT_DESCRIPTOR_FLAGS;

D3D12_ROOT_DESCRIPTOR_FLAGS

まずは、単体のDescriptorに付ける、D3D12_ROOT_DESCRIPTOR_FLAGSについてです。

D3D12_ROOT_DESCRIPTOR_FLAG_NONE

これは、設定するDescriptorがSRV/CBVならば、後述するDATA_STATIC_WHILE_SET_AT_EXECUTEと同等の扱いとなり、UAVならば、DATA_VOLATILEとして扱います。これは、デフォルトの挙動として用意されていますが、積極的に使う理由はありません。

D3D12_ROOT_DESCRIPTOR_FLAG_DATA_VOLATILE

こちらは、Descriptorが指し示すリソースが、Volatileであるとして扱います。リソースの変更は、ExecuteCommandList()が実行されれて、GPU側の実行が終了するまでの期間を除いて、CPU側からいつでも変更が可能です。
また、GPU側でも、このリソースを使うDrawcall/Dispatchの直前まで、他のGPUコマンドがこのリソースを変更する可能性があるとして扱います。
ちなみに、こちらのフラグの状態が、Root Signature Version 1.0と等価の動作となります。

D3D12_ROOT_DESCRIPTOR_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTE

これを設定すると、そのDescriptorがBindされ続けている間は、Descriptorが指し示すリソースに変更がないものとして扱われます。リソースの変更は、ExecuteCommandList()が実行されれて、実行が終了するまでの期間を除いて、CPU側からいつでも変更が可能です。
また、GPU側でも、そのDescriptorがBindされるまでは、たとえ同一のCommandList内でも、このDescriptorが指し示すリソースの内容の変更が可能です。さらに、Bindしたままでもデータの変更は可能ですが、後にそのリソースの読出しをする場合、このDescriptorを一旦Unbindして、再度Bindしなおす必要があります。

このフラグは、そのリソースがBindされている最中は、複数のDrawcall/Dispatchの境界で、そのリソースを読みだすためのキャッシュをInvalidateする必要が無いことを意味します。また、もしGPUがリソースのPrefetch動作をDrawcall/Dispatchの前に行うならば、このフラグが設定されているリソースに関しては、Bindされた直後の1回のみPrefetch動作を行えばよく、その後はキャッシュをInvalidateしないので、Prefecth動作が必要ありません。
ちなみに、そのDescriptorをBindしたままBundle境界をまたぐ場合は、各Bundle内で個々にこのルールが適用されるそうです。したがって、Bundle境界では読出し用のキャッシュはInvalidateされると考えてよさそうです。

D3D12_ROOT_DESCRIPTOR_FLAG_DATA_STATIC

これを設定すると、そのDescriptorが指し示すデータの内容が、CommandlistやBundleを作成時にBindされた時点から、変更が無いものとして扱われます。
データの変更は、参照しているCommandListやBundleの実行が終わるまで出来ません。そのCommandListやBundleを再度使用する場合も変更不可です。さらにBundleの場合は、CommandListへの設定時ではなく、Bundleの作成時から変更不可となります。加えて、Bundle内で使用するDATA_STATICが指定されているDescriptorは、Bundle内で明示的にBindされなければならず、CommandListから継承されません。ただし、CommandListが、Bundle内で設定された、Descriptorを使用することはできます。

言葉にするとややこしいのですが、これは、ドライバーがCommandListの作成時に、DATA_STACが指定されたリソースの内容を確定してしまうためによる制限です。つまり、Bundleを作成する場合は、CommandListから継承するリソースは不定なので、Bundle内で明示的にBindする必要があります。一方で、Bundleを呼び出しているCommandListを作成しているときは、Bundle内でのリソース設定状況は既知なので、戻ったあとのCommandListで問題なく使用できるというわけです。
このフラグは、特にCBVを指定するときに大きな効力を発揮します。RootSignature1.0では、D3D12_ROOT_CONSTANTSを使用する以外に、ドライバーやGPUに対して、事前に決定していて変更の無いシェーダーパラメータがあることを通知する方法がありませんでした。一方でRootSignature1.1では、CBVの内容全体に変更がないことが通知できます。
これによりドライバーやGPUは、このCBVの内容を事前にコピーして、各GPUアーキテクチャに応じて最もパフォーマンスを発揮できる場所に自由に格納することができるようになります。このように、ドライバーによる最適化が最大限発揮されるフラグとなります。可能であれば、積極的に設定するべきフラグです。

D3D12_DESCRIPTOR_RANGE_FLAGS

次に、DescriptorTableに付けるD3D12_DESCRIPTOR_RANGE_FLAGSについてです。

D3D12_DESCRIPTOR_RANGE_FLAG_DESCRIPTORS_VOLATILE

これは、当該DescriptorTable自体が、Volatileであるとして扱います。したがって、CommandListに、そのDescriptorTableが設定された後、ExecuteCommandListで実行されるまでの間に、Tableの内容が変更されるものとして扱われます。Tableの内容の変更は、ExecuteCommandList()が実行されれて、実行が終了するまでの期間を除いて、CPU側から、いつでも変更が可能です。ただし、DATA_VOLATILEと異なり、GPU側で変更はできません。これは元々あるDX12の仕様によります。
このフラグは、D3D12_DESCRIPTOR_RANGE_FLAG_DATA_VOLATILEもしくは、D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC_WHILE_SET_AT_EXECUTEと組み合わせて設定することができます。各々組み合わせることのできるフラグの意味するところは、各Descriptorが指すデータの変更条件を定義するのに対して、このフラグはDescriptorTable自体の変更条件を定義します。
また、このフラグのみ設定された場合は、各DescriptorがSRV/CBVならば、DATA_STATIC_WHILE_SET_AT_EXECUTEと同等の扱いとなり、UAVならば、DATA_VOLATILEと同等の扱いになります。これは、FLAG_DATA_NONEが設定された単体のDescriptorと同じです。

もし、このフラグを指定しなければ、当該DescriptorTableは、STATICであるとみなされます。このDescripotrTableがCommandListに設定されてから、
ExecuteCommandListが実行されるまでの間、DescriptorTable自体に変更がないものと仮定します。さらに、設定したCommandListを再利用する場合は、DescriptorTable自体の変更はできません。(ただし、各Descriptorが指すデータの内容は、各々のDATAフラグに応じて変更可能です。)
RootSignature1.0では、FLAG_DESCRIPTORS_VOLATILEと同様の動作でしたが、RootSignature1.1では、何も設定しないと、STATIC(フラグなし)がデフォルト動作となり、動作が大きく異なりますので注意が必要です。

このフラグを設定することの意味についてですが、もし、DescriptorTable自体が変更されないとすると、DescriptorTableの内容である、CBV/SRV/UAV_DESCが変更されないということです。これらの情報は、主にリソースの存在するGPUアドレスと、そのサイズです。もしもこれらが変更されないとすると、GPUやドライバはそのリソースが存在するページのアドレス変換テーブルなどを事前に準備できるということになります。また、アクセスされるリソースの大きさも事前に知ることができるので、アクセス範囲のチェックが必要なものに関しても事前に情報を集めることができます。
逆にこれらがDescriptorTableからもたらされるとすると、これらのチェックや設定は、GPUで実行時に、実際にDescriptorTableにアクセスして設定する他にありません。これがこのフラグを設定する意味になります。

まとめ – これらを設定する意味

一見すると、単に面倒な設定項目が追加され、しかも、大抵の場合はデフォルトの設定で正常に動作するように見えます。実際に、正常な動作という面では、全てにおいてVOLATILEの設定をしておけば、少なくとも、これらのフラグの設定が原因となる不具合を引き起こす可能性はありません。一方で、正しく設定した場合には、ドライバやD3D12のAPIは、早期にデータの内容や格納アドレスを決定することができ、それに伴って可能な限りの最適化を施すことができます。そして、このメリットを享受するためには、正しいフラグの設定が必要になります。
パフォーマンスが気になる場合は、以下の点に注意して正しいフラグの設定をお勧めします。

D3D12_DESCRIPTOR_RANGE_FLAG_DESCRIPTORS_VOLATILEは最小限にする

ただし、CommandListとDescriptorTableを再利用して、DescriptorTableの内容を書き換える場合はVOLATILEが必要です。一方で、毎フレームCommandListを構築するスタイルの場合は、DESCRIPTORS_VOLATILEを使う場面は少ないと思われます。
いずれにせよ、DescriptorTableに対するDefault動作はRootSignature1.0と1.1で大きく異なるので注意が必要です。

DATA_STATICやSTATIC_WHILE_SET_AT_EXECUTEを可能な限り付ける

SRV、特にCBVには可能な限りDATA_STATICを付けることをお勧めします。
RootSignature1.0の頃より、アップデートの頻度が大きく異なる情報は、同一のCBVに格納せずに、分けることがお勧めされてきましたが、これは引き続きRootSignature1.1でも同様です。これに加えて、RootSignature1.1ではDATA_STATICが指定可能なデータというのも、CBVを設計する上で考慮する要素になると思います。

最後に

全てのWidnows10でRootSignature1.1が使用可能ではないので、下記の関数で、RootSignature1.1が使用可能かを確認することをお勧めします。

typedef struct D3D12_FEATURE_DATA_ROOT_SIGNATURE {
  D3D_ROOT_SIGNATURE_VERSION HighestVersion;
} D3D12_FEATURE_DATA_ROOT_SIGNATURE;
D3D12_FEATURE_DATA_ROOT_SIGNATURE fd;
HRESULT CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &fd, sizeof(fd));
広告