日別アーカイブ: 2007年8月31日

Direct3D 10 Shader4.0 APIによってコンパイラが違う

同じシェーダーで、かつ同じコンパイルオプションを指定しているのに、
生成されたシェーダーのバイナリが完全に同一になりません。
こんな症状にしばらく悩んでいました。

判明した結論は下記のとおり。

「Direct3D10 は使用する API によって HLSL コンパイラが異なっている」

この違いは April2007 から生じており、当時のリリースノートを
良く読むとそれらしいことが書いてありました。
(マニュアルの「What’s New in the April 2007 DirectX SDK」)

以下、HLSL Compile 系 API の違いなどを説明しながら調べた結果を
書いてみます。

DirectX10 では Effect(fx) や HLSL 関連の API も core に含まれる
ようになりました。そのため必ずしも D3DX を使う必要は無く、
エフェクトのコンパイルから生成まで core API だけで実現する
ことができます。

それでも D3DX には Shader や Effect のコンパイル関連の API が
数多く用意されています。core API とほとんど同じ名前で似たものが多く、
あまり違いが無いように見えます。
では D3DX の Compile 系関数を使う利点はどこにあるのでしょうか。

  (1) Compile → Create の手順が一度に出来る。
  (2) ファイルやリソースから直接読み込める。
  (3) 非同期実行できる。(ID3D10ThreadPump)
  (4) #include に対応している。

core 側の API は Compile と Create の API が分かれています。
例えば ID3D10Effect の場合

 1. D3D10CompileEffectFromMemory( .. )
 2. D3D10CreateEffectFromMemory( .. )

と2ステップ必要になります。API が分かれることで、
コンパイル済みバイナリをファイル保存できるし、
コンパイルされたバイナリを読み込めば Compile 無しに Create
だけ通すことも出来ます。

Compile 後のバイナリをそのままファイルに書き出せば、
fxc.exe で作った fxo ファイルと全く同じものができました。

ちなみに API は FromMemory しかなく、FromFile も FromResource
もありません。以前は D3DX に含まれていたことから、わかり
やすいように関数名だけ継承したものと思われます。

D3DX の場合は洗練された汎用性よりも便利さ優先で、下記の命令で
いきなり ID3D10Effect のインスタンスを生成することが出来ます。

 D3DX10CreateEffectFromFile( … )
 D3DX10CreateEffectFromMemory( … )
 D3DX10CreateEffectFromResource( … )

メモリだけでなく直接ファイルやリソースから作ることも出来ます。

ちなみにマニュアルにはミスがあって、最後の引数

  HRESULT *pHResult

が正しく載っていないものがあります。これは ID3DX10ThreadPump で
非同期実行したときに、実行結果を受け取るためのバッファです。
非同期実行しない場合は関数の戻り値で結果がわかるので、この
パラメータは NULL で構いません。

また core API は内部で勝手にファイルアクセスされると困るので、
HLSL 内の #include ディレクティブに対応していません。
#include を意図したとおりに機能させるには、自前でインプリメント
した ID3D10Include が必要です。
#include 以外のプリプロセッサコマンドはそのまま通ります。

D3DX の関数では何もしなくても、内部で勝手に ID3D10Include を
用意してくれます。
もちろん自分で定義した ID3D10Include を渡すことも出来ます。

マニュアルを見ると FromFile の場合のみ自動で #include を処理
してくれるようなことが書いてあります。FromMemory/Resource
ではユーザー定義の ID3D10Include に頼らなければいけないように
見えます。ところが実際に試してみると、

 D3DX10CreateEffectFromMemory()
 D3DX10CompileFromMemory()
 D3DX10PreprocessShaderFromMemory()

どれも ID3D10Include 無しに #include 可能でした。実際、下記の
コードの代わりに

D3DX10CreateEffectFromFile(
	"sysdef.fx",
	NULL,	// macro
	NULL,	// include
	"fx_4_0",
	0,	// HLSL flags
	0,	// FX flags
	g_iDevice,
	NULL,	// EffectPool
	NULL,	// ThreadPump
	&iEffect,
	&blobError,
	NULL	// HResult
	)

FromMemory を使った次の書き方をしても動きました。

const char*	memory= "#include \"sysdef.fx\"\n";
D3DX10CreateEffectFromMemory(
	memory,
	strlen( memory ),
	fxFileName,
	NULL,	// macro
	NULL,	// include
	"fx_4_0",
	0,	// HLSL flags
	0,	// FX flags
	g_iDevice,
	NULL,	// EffectPool
	NULL,	// ThreadPump
	&iEffect,
	&blobError,
	NULL	// HResult
	)

FromResource() もそのままで #include 処理出来るかもしれません。

このように、一見重複に見えるけど D3DX 側には便利な関数が
それなりの理由をもって用意されているわけです。
と、最初はこのくらいに考えていました。

ところが実際は、大きく分けて下記の2種類の HLSL コンパイラが
共存していることになります。

・core API が呼び出す HLSL コンパイラ
・D3DX が呼び出す HLSL コンパイラ

ID3D10Shader の D3D10_SHADER_DESC Creator を見るとこの両者の
違いがわかります。

・D3D10CompileEffectFromMemory() の場合
     Microsoft (R) HLSL Shader Compiler

・D3DX10CompileFromMemoryD3DX()  (August2007) の場合
     Microsoft (R) HLSL Shader Compiler 9.19.949.1104

D3DX が呼び出すコンパイラは C:\Windows\System32 以下にある
dll です。それぞれ内部のバージョン番号も記してみました。

D3DCompiler_33.dll    9.18.949.0015 (April2007)
D3DCompiler_34.dll    9.19.949.0046 (June2007)
D3DCompiler_35.dll    9.19.949.1104 (August2007)

D3DX の Compile 系 API を呼び出したときだけこれらの dll が
読み込まれていることが確認できます。
core API が呼び出しているコンパイラは上記よりもさらに古い
バージョンとなるわけです。

April2007 のリリースノートを見ると bug fix や最適化など
いろいろ修正が入ったようです。

すべての API で新しいコンパイラに置き換わらないのは、
おそらく core 側に入ったことで、安易に変更出来なくなった
ためでしょう。

ちなみに fxc.exe のコマンドラインには

Microsoft (R) D3D10 Shader Compiler 9.19.949.1075

と表示されますが、生成されたバイナリは 9.19.949.1104 となり
D3DX 側のコンパイラが呼ばれていることがわかります。

実際にコンパイルされた結果を見ると、わかる範囲でですが
若干テンポラリレジスタの割り当てが変更されており、
ConstantBuffer もシェーダーで参照している分しか宣言しない
ようになっていました。

●結論

D3DX 側関数を使う理由にもう1つ

  (5) 最新の HLSL コンパイラを使うことが出来る

がありました。HLSL のコンパイルは fxc.exe か D3DX の関数を
使いましょう。core API 側の関数を使うと古いバージョンの
コンパイラを呼び出してしまいます。
(そしてSDKのリリースノートはちゃんと読んでおきましょう・・)