Direct3D11/DirectX11 (5) WARP の試し方、Dynamic Shader Linkage

DirectX SDK November 2008 Technical Preview 5回目です。
WARP の試し方の補足と、D3D11 の大きな目玉のひとつである
「動的なシェーダーリンク」について。

● Direct3D 11 が Direct3D 10 より多くのハードで動く理由

  (1) Direct3D 9 世代の GPU に対応した
  (2) ソフトレンダラ WARP が追加された

(1) ShaderModel 2.0~3.0 でも API 自体は動きます。
ただし固定パイプライン用機能が存在しないため Shader 必須。
D3D11 になったからといって GPU の能力を超えて出来ることが増える訳じゃないので、
使えない機能多数。逆に使用できないステートなど制限が生じる可能性あり。

(2) GPU に必要な 3D 機能が無くても WARP を使えば CPU だけで動作します。
DirectX SDK November 2008 現在 10.1 相当で、Reference Driver よりずっと高速。

●手っ取り早く WARP を試す方法

install したサンプルの DXUT.cpp 2907行あたりに
pDeviceSettings->d3d11.DriverType= D3D_DRIVER_TYPE_WARP;
を挿入します。下記の赤い行。WARP Driver で起動するようになります。

// DXUT.cpp 2902行~
        if( GetDXUTState().GetOverrideForceREF() )
            pDeviceSettings->d3d11.DriverType = D3D_DRIVER_TYPE_REFERENCE;
        else if( GetDXUTState().GetOverrideForceHAL() )
            pDeviceSettings->d3d11.DriverType = D3D_DRIVER_TYPE_HARDWARE;

        pDeviceSettings->d3d11.DriverType= D3D_DRIVER_TYPE_WARP;

起動直後 = WARP
オプション画面から REFERENCE or HARDWARE に切り替え可能。(WARP には戻せない)

WARP device も FeatureLevel 10.1 なので、10.1 で動かないサンプルは動きません。
DirectX SDK November 2008 現在、動くのは MultithreadedRendering11 だけです。

実行速度は CPU 速度に依存します。

● Dynamic Shader Linkage

Direct3D 11 の大きな特徴の一つがこの Dynamic Shader Linkage です。

動的なリンクというと、複数のシェーダーバイナリをリンクし相互参照を解決する
イメージがありますが・・違います。

わかりやすく言えば「シェーダー内で関数ポインタが使えるようになった」ということ。

インスタンス自体は静的に生成されており、単一のシェーダーバイナリにすべての
インスタンス及びエントリポイントが含まれていなければなりません。

一つのシェーダープログラムの中に複数の関数を作成することが出来、その飛び先を
動的に切換えることができるわけです。

これらの仕組みは HLSL 上では class と interface の形で用いられます。

interface Base {
	float4	GetColor();
};

class Red : Base {
	float4	effect;
	float4	GetColor()
	{
		return	float4( 1, 0, 0, 1 );
	}
};

class Green : Base {
	float4	effect;
	float4	GetColor()
	{
		return	float4( 0, 1, 0, 1 );
	}
};

cbuffer ibuf {
	Red	cbRed;
	Green	cbGreen;
};

Base	color;	// この宣言はポインタ相当で実体を持たない

float4 main() : SV_Target
{
	return	color.GetColor();
}

上の例では Green と Red が interface Base を継承して作られています。
実行は Base で宣言された color を経由して行われており、どのメソッドが
呼び出されるのかコンパイル時にはわかりません。

Green 及び Red のインスタンスは cbRed, cbGreen として ConstantBuffer 上に
静的に作られています。外部の C++ 側からは、このインスタンス化された “cbRed”、
“cbGreen” を参照することが出来ます。

実際にどのインスタンスを color に割り当てるのか、レンダリング時に C++ 側で
いつでも切換えられるわけです。

シェーダー内のインスタンス参照には ID3D11ClassLinkage::GetClassInstance()
を使います。

ID3D11ClassLinkage* iClassLinkage;
iDevice->CreateClassLinkage( &iClassLinkage );
iDevice->CreatePixelShader( blob, size, iClassLinkage, &iPS );

ID3D11ClassInstance* iClassRed;
ID3D11ClassInstance* iClassGreen;
iClassLinkage->GetClassInstance( "cbRed", 0, &iClassRed );
iClassLinkage->GetClassInstance( "cbGreen", 0, &iClassGreen );

ID3D11DeviceContext::PSSetShader() で、シェーダーと同時に使用する
インスタンスのリストを渡します。

// Green を呼び出す場合
iContext->PSSetShader( iPS, &iClassGreen, 1 );

Shader 内には複数の interface が宣言されている可能性があるので、必ずしも
上のように単純になりません。Reflection を参照して、PSSetShader() に渡す
インスタンスリストの何番目がどの interface 呼び出しに対応するのか調べる
必要があります。

class に変数が含まれている場合、ConstantBuffer 内のメモリ配置との対応付けも
必要です。(この場合 Effect ではないのでバッファの作成も必要)

Dynamic Shader Linkage の仕組みは、このようにパフォーマンスに影響が出ないよう
慎重に作られているため扱いは必ずしも簡単ではないようです。
今後 Direct3D11 対応の Effect fx_5_0 が完成したらある程度自動で行ってくれる
ようになるかもしれません。

出力されたコードを見ると fcall 命令が使われています。label の値 (おそらく
オフセット) を用いてサブルーチン呼び出しを行う仕組みだと考えられます。

ps_5_0
dcl_globalFlags refactoringAllowed 
dcl_function_body fb0
dcl_function_body fb1
dcl_function_table ft0 = {fb0}
dcl_function_table ft1 = {fb1}
dcl_interface fp0[1][1] = {ft0, ft1}
dcl_output o0.xyzw
dcl_temps 1

fcall fp0[0][0]
mov o0.xy, r0.xyxx
mov o0.zw, l(0,0,0,1.000000)
ret 

label fb0
mov r0.xy, l(0,1.000000,0,0)
ret 

label fb1
mov r0.xy, l(1.000000,0,0,0)
ret

上記のように飛び先と種類はコンパイル時に決定しており、おそらく外部参照はできません。
このことは次の例を見てもわかります。

interface Base {
	float4	GetColor();
};

class Blue : Base {
	float4	GetColor()
	{
		return	float4( 0, 0, 1, 1 );
	}
};

Base	color;

float4 main() : SV_Target
{
	return	color.GetColor();
}

この場合 interface を利用しつつも、継承されたエントリが Blue しかありません。
驚くことに HLSL コンパイラは Blue 以外が決して呼ばれないことを認識し、
間接呼び出しを取り除きます。
つまり main の中には Blue::GetColor() がインライン展開されてしまいます。

class の宣言自体は ShaderModel 4.1 (FeatureLevel 10.1) 以前でも使えます。
ただし間接呼び出しは出来ないので、interface からの呼び出しはエラーとなります。
上の例だと「Base color;」を「Blue color;」に書き換えればコンパイル可能です。

ちなみに従来の 4.1 以前のシェーダーにもサブルーチン呼び出しの機能自体は存在
していました。しかしながら HLSL コンパイラはすべてをインライン展開し尽くすので
実際に使われることはほとんどありません。
自分が知る限り、実際に関数呼び出しのコードが出力されるのは switch 文の
attribute に [call] を指定した場合だけでした。

Direct3D11 になってようやくシェーダー内のサブルーチン呼び出しが意味を持つ
ようになりました。

関連エントリ
Direct3D11/DirectX11 (4) FeatureLevel と旧 GPU の互換性、テクスチャ形式など
Direct3D11 (DirectX11) シェーダーの書き込み RWBuffer 他
Direct3D11 の遅延描画、スレッド対応機能、シェーダー命令
Direct3D11 Technical Preview D3D11の互換性、WARP Driver