Direct3D 12を始める – Command

Windows 10 Technical Preview向けのWindows SDKには、最新のDirectXのライブラリが同梱されていますその中でもまだ情報が少ないDirect3D 12に焦点を当て、筆者が調べたことをまとめて記事にしていきます。

現時点でSDKはプレビュー版であり、リリース版では仕様変更の可能性があります。
書きかけにつき今後図を追加するかもしれません。


■コンテクストからコマンドへ

Direct3D 11では、DeviceContextにリソースビューとステートオブジェクトを複数設定し、DrawCallをサブミットしていました
Direct3D 12では、CommandListに複数のデスクリプタ、1つのRootSignature、1つのPSO(パイプラインステートオブジェクト)を設定し、DrawCallし、それを必要な回数繰り返してからCommandQueueにサブミットします。

ここでは、DeviceContextに代わるD3D12のコマンドについて掘り下げます。


■Command List

DeviceContextに代わり、GPUに命令を送るときの情報を蓄える仕組みがCommand Listです。

Command Listの役割はDeviceContextとほぼ同じです。
D3D11では、Deviceがスレッドセーフ、DeivceContextが非スレッドセーフなAPIとして設計されていました。
D3D12でも、Deviceはスレッドセーフ、CommandListが非スレッドセーフなAPIとなっています。

DeviceContextにはいくつかの問題がありました。

D3D11のDeviceContextは、Immediate ContextとDeferred Contextの2種類がありました。
Immediate Contextは1つしか存在せず、コマンド構築をマルチスレッドで行うことが不可能でした。
GPUの性能はまだ伸び続けていますが、CPUのシングルスレッド性能は伸び悩んでいるため、GPUを空転させる可能性が高くなってきています。
また、Deferred Contextのサブミットには必ずImmediate Contextを介する必要があり、Context間で発生するリソースの依存性解決がドライバに大きな負荷を掛けていました。

実際、ドライバがDeferred Contextを真にサポートしたのは、NVIDIAだけでした。
ただし、使用にはドライバの特性に気を付ける必要がありました。(参考文献1)
IntelやAMDの場合、Deferred Contextを使う利点がありませんでした。

一方、Command Listは、スレッド間の依存性がありません。
Command List間でやり取りするAPIはなく、GPUのステートはCommand Listの開始前にリセットされます。
Command ListをCPUのスレッド数用意し、並列にコマンド構築することで、マルチコアの恩恵を最大限受けることができます。

Command ListはID3D12Device::CreateCommandList()から作成します

Command Listはレコード状態とクローズ状態があります。
作成直後はレコード状態で、ID3D12GraphicsCommandList::Close()を呼び出すとクローズ状態となり、ID3D12GraphicsCommandList::Reset()を呼び出すとレコード状態に戻ります。
クローズ状態では、一切のコマンドを呼び出すことができません。


■Comnand Queue

GPUに描画を開始させるには、グラフィックドライバからGPUにコマンドを転送する必要があります。
しかし、DrawCallのたびにGPUと通信するのはCPUコストが高くなるため、最近のグラフィックスAPIは複数のDrawCallをまとめて一括でコマンドをサブミットするのが一般的です。

D3D11では、このサブミットは暗黙的に行われていました。
D3D12では、サブミットを明示的に行います(ただしハードウェアキューと直接対応しているかはおそらく実装依存)。

Command QueueはID3D12Device::CreateCommandQueue()から作成します
MSDNにあるサンプルコードではID3D12Device::GetDefaultCommandQueue()から作成している例がありますが、現在のWindows 10 SDKでは廃止されているようです。

Command ListをCommand Queueにサブミットするには、クローズ状態にする必要があります。
クローズ状態のCommand Listは、ID3D12CommandQueue::ExecuteCommandLists()でサブミットできます。

CommandList_CommandQueue
Command ListとCommand Queueの関係

Queueは”Direct Queue”と”Compute Queue”と”Copy Queue”の3種類があります。
Direct Queueは、すべてのCommand Listをサブミットできます。MSDNドキュメントでは”Graphics Queue”や”3D Queue”と書かれることがあります。
Compute QueueはCompute ShaderとCopy系コマンドのみを実行でき、Copy QueueはCopy系コマンドのみを実行できるQueueですが、現在のWindows 10 SDKでは未実装でした。

いずれのQueueも作成時にタイプを指定します。
Command Listも作成時にタイプを指定します。
たとえば、DirectタイプのCommand ListをDirectタイプのCommand Queueにサブミットすることができますが、ComputeタイプのCommand Queueにサブミットすることはできません。

Command Queueの消化はサブミットされた順に行われます。[1]
Command QueueがあるCommand Listを実行しているとき、別のCommand Queueは割り込めません。
ただし、異なるQueueにCommand Listをサブミットした場合、順番は保障されません。
それだけでなく、並列で実行される可能性もあります。
GCN世代のRadeonにはACE(Asynchronous Compute Engine)が2つ或は8つ搭載されており、複数のComputeタスクが並列実行されます。
描画タスクは並列実行されません。

Queueには優先度をつけることができます。
ハードウェアが並列実行できるタスク数を上回るサブミットが行われた場合、優先度の高いQueueから実行されます。

また、キューにTDR(シェーダの無限ループ等によりGPUがハングアップしたことを検出してドライバを再起動させるWindowsの仕組み)を無効にさせるフラグがあり、シェーダデバッグ時のステップ実行などに活用できるようです。
おそらく製品で使ってはいけないと思います。


■Command Allocator

Command Listがハードウェアネイティブな描画コマンドに変換され、グラフィックスメモリ上に確保される領域を確保するインターフェイスがCommand Allocatorです。

Command AllocatorはID3D12Device::CreateCommandAllocator()から作成します

Command Listの作成時、またはReset()によりレコード状態に戻すときは、必ず1つのCommand Allocatorとバインドする必要があります。
Command ListをClose()したとき、バインドされたCommand Listに描画コマンドが変換され書き込まれます。

Command AllocatorはID3D12CommandAllocator::Reset()でクリアすることができます。
Command Allocatorがどれだけのグラフィックスメモリを消費したかを知る術はありませんが、全く同じ描画コマンドの記録とReset()を繰り返した場合、2回目以降のメモリの消費量が変動しないことが保証されています。

Command AllocatorのReset()は、必ず、すべてのバインドされたCommand Listがクローズ状態になっていなければなりません。
また、今までバインドされたすべてのCommand ListがGPUでの実行を終えるまで、Reset()を呼び出してはいけません。
この順序を守らないと、デバイスのリムーブが発生し、継続動作できなくなります。

GPUの実行状態を検知するためにFenceという仕組みが用意されていますが、ややこしい話なのでここでは説明しません。

Command ListをReset()するとき、バインドするCommand Allocatorを前のCommand Allocatorと別のものに変えることができます。[2]
このとき、指定するCommand Allocatorは作成直後かReset()済みでなければなりません。
逆に、作成直後かReset()済みのCommand Allocatorならば、前にバインドしたCommand AllocatorがまだGPUで使用中であっても、直ちにReset()を呼んでも問題なく動作します。

また、バインドされたCommand Allocatorは、同時に2つ以上のCommand Listをレコード状態にすることができません。[3]

つまり、最大のパフォーマンスを発揮するには、Command Listをスレッドの数以上、Command Allocatorをスレッドの数×2以上作る必要があります。

ただとりあえず動かすだけであれば、Command ListのClose、Command ListをCommand Queueへサブミット、Fenceで同期待ち、Command AllocatorのReset、Command ListのReset、といった手順を踏めばよいようです。


■まとめ

Command List、Command Queue、Command Allocatorを紹介しました。

■ソース

  1. Any thread may submit a command list to any command queue at any time, and the runtime will automatically serialize submission of the command list in the command queue while preserving the submission order.  [URL]
  2. A typical pattern is to submit a command list and then immediately reset it to reuse the allocated memory for another command list. [URL]
  3. Note that only one command list associated with each command allocator may be in a recording state at one time. [URL]

■参考文献

  1. D3D11 Deferred Contexts Primer & Best Practices (Bryan Dudash, GDC2013)
広告

コメントを残す

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

WordPress.com ロゴ

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

Twitter 画像

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

Facebook の写真

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

Google+ フォト

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

%s と連携中