IDF14で公開されていたGDC2014からのDierct3D 12のアップデート

Intelが2014年9月に行ったIntel Developer Summit 14で、Direct3D 12に関する情報がアップデートされていたことが分かりました。

ソース:IDF2014の資料ダウンロードページ

以前、筆者はGDC2014の資料を基にSlideshareでDirect3D 12についてまとめた資料を公開しましたが、ほとんどはその資料と同じ内容です。

しかし1点だけ、リソースの設定機構については、基本的な設計から見直されており、全くの別物になっているようです。
資料に”Need more flexible parameterization”と書かれていることから、GDC2014の時点からIDF2014までの間に設計変更があったことが伺えます。

■おさらい – GDC2014の時点でのリソースの設定

GDC2014時点でのリソースの設定機構(OpenGLのように「バインド」と呼ぶ)は、巨大なDescriptorHeapをVRAMから確保し、Descriptor単位で切り出してバインド先を記録し、それをDescriptorTableで参照することでシェーダに紐づける、という仕組みでした。

図では、シェーダステージ間でDescriptorを共有できるような描かれ方をしていましたが、誰得な気がします。

■すべてのリソースにViewの概念がつく

これもほぼおさらいですが、より厳密な表記がされているので、先にまとめておきます。

Direct3D 11ではSRV(ShaderResourceView)、UAV(UnorderedAccessView)、RTV(RenderTargetView)、DSV(DepthStenilView)というViewが存在します。
いずれもcreateBuffer()やcreateTexture()で作ったリソースをシェーダでどう扱うか、その型と範囲を決定するために使われていました。

Direc3D 12ではVRAM(Direct3D 12用語ではHeap?GDC2014では「アロケータチャンク」という表現も見られる)をアプリケーションが自由に扱えるためか、すべてのHeapにViewの概念が追加されています。

CBV(ConstantBufferView)
IBV(IndexBufferView)
VBV(VertexBufferView)
SOV(StreamOutputView)

CBVについては、Direct3D 11.1で似た概念が追加されています。
しかし、それはSetConstantBuffers1()で設定の度に範囲を指定するというもので、Viewが存在したわけではありませんでした。
Direct3D 12では、Descriptorに設定するリソースは(インデックスバッファのようにシェーダステージで明示的に使うことがないものであっても!)すべてViewを通してアクセスする仕組みになります。

ただし、SamplerだけはDX11と変わらないようです。

■Root Signature & Root Parameter

GDC2014からの最大の変更点は間違いなくこれです。
“Arrayed indexing of multiple descriptors and constants not allowed”とあることから、GDC2014のリソース設定機構はボツになったと見て間違いなさそうです。

Direct3D 12の新しいバインドの仕組みでは、頻繁に更新されるパラメータにGPUのレジスタやリネーミングパスが効率よく動作できるようにするため、シグネチャパラメータという2つの概念が追加されました。

Root Signatureは、Pipeline(全てのシェーダステージをまとめたもの、いわゆるPipeline State Object(PSO))と対になっていて、Pipelineの型に合わせてアプリケーションが作る必要があるものです。
構造は以下のようになっています。

RootSignature
├Descriptor Tables
├Descriptors
└Constants

RootSignatureに設定するDesciptorsやConstantsの型宣言を”Root Descriptors”や”Root Constants”と呼ぶようですが、資料の中ではやや曖昧です。

RootSignatureに設定する値をRootParameterと呼びます。

コマンドバッファの中でこのRoot Signatureを変更されると、Pipeline StateでのParameterの再利用効率が落ちるため、変更は最小限にしなさい(”Minimize signature changes”)、と書かれています。
RootSignatureをどう設計するかが、Direct3D 12アプリケーションのパフォーマンスに大きな影響を与えるようです。

なんとなくOpenGLのUniform Buffer Objectの設計に似ている気がします。

■Descriptor Tables & Descriptors & Constants

Descriptor Tablesと、Descriptors・Constantsの2つは、明確に用途が異なります。

Descriptorsは、Viewが設定できます。
つまり、SRV/CBV/UAV/Samplerを設定するために使います。

Constantsは即値をそのまま設定します。
FLOATとUINTとSINTが設定できます。
HLSL registerのb0-b15に直接アサインされるようです。
1DWORDだけ設定できて何がうれしいのか疑問に思いますが、資料の中に次のようなコードがあることから推測できるかもしれません。

cbuffer DrawConstants
{
    UINT ConstantBufferOffset;
} : register(b0)

一方、DescriptorTablesは、DescriptorかConstantsへの参照を持ちます。
GDC2014の資料とほぼ同じ役割です。
ただし、”Table can identify mix of CBV/SRV/UAVs”という表記から、1つのDescriptorの中にSamplerこの混在はできないようです。

idf2014_direct3d12_32

(Microsoft Direct3D 12: New API Details and  Intel Optimizations, p.32)

idf2014_direct3d12_34

(Microsoft Direct3D 12: New API Details and  Intel Optimizations, p.34)

上の図を見ると、「Descriptor Tablesなくてもリソースバインドできるじゃん」と思います。
実際それも許されると思います。

しかし、Root Constant / Root Descriptorが増えれば、描画のたびに設定するDescriptorも大きくなりCPUの転送時間かGPUのストール時間かのどちらかが増えるのでしょう。
「静的なリソースはDescriptor Tableを使え」「巨大なDescriptor Tablesを作って可能な限りDrawCall間で共有せよ」と書かれています。
また、「動的に更新される定数バッファは、GPUのシェーダ遅延を省くためにRoot Constantsを使い、Root Constantsを使い切ったらRoot Descriptorを使え」とも書かれています。

メッシュのようにImmutableなものはDescriptorTableで、DynamicなものはConsntant / Descriptorで、というのが基本戦略のようです。
これはCommandListsとBundlesの関係に似ているのでしょう。

このように、DX12のリソースバインドはDX11のSet***()と違って非常に柔軟な(ただしユーザが手間をかける必要がある分面倒くさい)設計になっています。

■RootSignatureの容量制限

実は、Root Desciptor Tableを活用せざるを得ない理由がもう一つあります。

Root Signatureには「16DWORDまで」という非常に厳しい制限があります。
また、Root Parameterが消費する容量も厳密に定義されています。
Root Descriptor Tableは1DWORD、Root Descriptorは4DWORD、Root Constantは1DWORD*必要な数、となっています。

つまり、Root DescriptorはRoot Signatureに最大4つしか入りません
定数バッファの場合でもfloat4x4を1つ使った時点でRoot Signatureを使い切ります

この時点で、Root Desciptor Tableに頼るのはマストであるといえます。

■Descriptor Tables /Descriptors / Constantsの設定

Root Signatureに対応したDescriptorたちは、CPUから書き込め、シェーダからアクセスできるHeapに用意します。
このHeapへの書き込みは、GPUが使用中でなければ(たとえコマンドバッファへのDrawCall等の記録が終わった後でも)可能な一方、その管理はアプリケーションの責任となっています。
描画中に書き込んだら、たぶんデバイスリムーブが待っているのでしょう。
そのため、「アプリケーションがDesciptorをバージョン管理しなさい」と書かれています。

面倒くさい話です。

なお、シェーダから参照されないIBV / VBV / SOV / RTV / DSVについては、CPUから任意に読み込むことも許可されています。
RTVに適当な演算結果を書いて、StagingバッファなしでいきなりMap()して値を取ってこれるのだと思います。
とはいえCompute Shaderで使うUAVや、SRV / UAVもCPU Write Onlyなので、たいていのケースではStagingバッファのようなものを自分で作ることになるでしょう。

■サンプルコード

PDF資料の中から抜粋しました。
一部表記を省略しています。

1. RootSignatureの作成

struct D3D12_ROOT_PARAMETER {
    D3D12_ROOT_PARAMETER_TYPE ParameterType;
    union {
        D3D12_ROOT_DESCRIPTOR_TABLE DescriptorTable;
        D3D12_ROOT_CONSTANTS        Constants;
        D3D12_ROOT_DESCRIPTOR       Descriptor;
    }
    ...
}

D3D12_DESCRIPTOR_RANGE DescriptorRange[2];
D3D12_ROOT_PARAMETER   Params[4];
ID3D12RootSignature* pRootSignature;

DescriptorRange[0].Init(D3D12_DESCRIPTOR_RANGE_SAMPLER, 2, 0);
DescriptorRange[1].Init(D3D12_DESCRIPTOR_RANGE_SRV, 5, 2);

Params[0].InitAsDescriptorTable(1, &DescriptorRange[0]);
Params[1].InitAsDescriptorTable(1, &DescriptorRange[1]);
Params[2].InitAsConstantBufferView(7);
Params[3].InitAsConstants(1, 0);

pDevice->CreateRootSignature(Params, ARRAYSIZE(Params), &pRootSignature);

D3D12_DESCRIPTOR_RANGEがDescriptorTableの中身の定義だと思います。
RootSignatureのスロット0に複数のSamplerを指すDescriptorTableを、スロット1に複数のSRVを指すDescriptorTableを、スロット2にCBVを、スロット3に即値を渡しているようです。

この書き方だと、DescriptorTableの中身がSRV/UAV/CBVのいずれかでなければならないように見えるので、第2引数と第3引数に何かありそうですが、これだけではよく分かりません。

2. Pipeline State Objectの作成

D3D12_GRAPHICS_PIPELINE_STATE_DESC PipelineDesc;
ID3D12PipelineState*               pPipelineState;

pDevice->CreteGraphicsPipelineState(pRootSignature, &PipelineDesc, &pPipelineState);

RootSignatureがPipelineStateObjectより上位の概念であることが分かります。

3. Root Signatureとパラメータの設定

pCommandList->SetGraphicsRootSignature(pSignature);

pCommandList->SetGraphicsDescriptorTable(0, SamplerTableHandle);
pCommandList->SetGraphicsDescriptorTable(1, TextureTableHandle);
pCommandList->SetGraphicsRootConstantBufferView(2, CBVHandle);
pCommandList->SetGraphicsRoot32bitConstant(3, ConstValue, 0);

1.のParamsとの対になっているのが分かります。

CommandListについては先のSlideshareを参考にしてください。ここでは説明を省きます。

4. ドローコール

pCommandList->SetPipelineState(FirstPipelineState);
pCommandList->SetGraphicsRoot32bitConstant(0, FirstCBVOffset, 0);
pCommandList->Draw(...);

pCommandList->SetGraphicsRoot32bitConstant(0, NextCBOffset, 0);
pCommandList->Draw(...);

pCommandList->SetPipelineState(LastPipelineState);
pCommandList->SetGraphicsRoot32bitConstant(0, LastCBVOffset, 0);
pCommandList->Draw(...);

ConstantBufferOffsetの伏線がここで回収されます。
Root ConstantでCBVをまるでポインタへのアクセスのように使っています。
おそらく結果はDrawInstanced()と同じようなものになるでしょう。
ただしこちらは、Graphics Pipelineの変更もはさむことができます。

■Direct3D 11ハードウェアとの互換性

Direct3D 11.2のTiled Resourceにあったのと同様に、リソースバインドにもTierが存在します。

Tier1 : Direct3D 11と同じ制限、Heapが2^16、Descriptor Tableが5つまで
Tier2 : Heapが2^20、SRVとSamplerが無制限、PSOあたりのUAVが64まで
Tier3 : Heapが2^20以上、すべてのリソースが無制限

DX12はDX11ハードウェアでも動作できるのでTierで区切られてしまうのは少々残念ですが、Descriptor Tablesが無制限に使えるわけではない点には注意が必要そうです。

なお、Intel GPUの場合、”Feature Level 12″は”Next Generation Intel Core processors”と”Next Gen 14nm Intel processor for Tables”で使えるそうです。
おそらく、Skylake / Braswellで対応し、Haswell / Broadwell / Bay Trail / Cherry TrailはTier1なのでしょう。

■まとめ

Root Signatureに関するDirect3D 12の新しいリソースバインドについて説明しました。

ちなみに明日からGDC2015が始まり、そこでDirect3D 12とOpenGL Nextに関する新しい情報が公開されるらしいので、必見です。
ここでRoot Signatureが跡形もなく消えていたら…泣きます。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中