Direct3D 10 HLSL Effect/FX リソース設定のはまり

●前置き

D3D10/DX10 のレンダリングは、入力と出力に同じものを指定することが
できません。

同じバッファ (Texture) を、RenderTarget と ShaderResource(Texture)
に同時に設定できず、もし設定しても入力側が強制的に 0 (NULL) 相当に
なります。少々厳密すぎる気もしますが、自己レンダリングは動作結果を
保障できないので当たり前といえば当たり前です。

非常にありがたいことに Direct3D10 では、間違って設定しても
DebugLayer が親切に教えてくれます。

ただ判定が厳密すぎるために少々問題になることもあります。

例として、テクスチャへのレンダリングを考えてみます。次のように
読み込み用と書き込み用の View を作成してあるものとします。
iTextureBuffer と iRenderBuffer は、同一の Resource Buffer を
参照していると思ってください。

ID3D10ShaderResourceView*  iTextureBuffer[2]; // テクスチャ
ID3D10RenderTargetView*    iRenderBuffer[2];  // レンダーターゲット

// iTextureBuffer[0] と iRenderBuffer[0] はどちらも Resource[0]
// iTextureBuffer[1] と iRenderBuffer[1] はどちらも Resource[1]

下記の “InputTexture” は Effect(fx) 内で Texture2D 宣言された変数で、
入力するテクスチャを意味しています。登録用インターフェースを
iInputTexture にキャッシュしておきます。

ID3D10EffectShaderResourceVariable* iInputTexture=
    iEffectModel->GetVariableByName( "InputTexture" )
        ->AsShaderResource();

描画時のセットアップを次のようにします。

// list1
// 入力テクスチャの設定
iInputTexture->SetResource( iTextureBuffer[0] ); // ←入力 (A)
iEffectModel->GetTechniqueByName( "Main" )
      ->GetPassByIndex(0)->Apply(0);

// 出力の設定
iDevice->OMSetRenderTargets( 1, &iRenderBuffer[1], NULL ); // ←出力 (B)

Effect の変数に登録したパラメータは Apply() で反映されます。
これで Draw() を発行すると、Resource[0] を読み込んで Resource[1]
を更新することができます。

Resource[1] ← Resource[0]

その直後に、今度は入力と出力を入れ替えてレンダリングします。

Resource[0] ← Resource[1]

// 入力テクスチャの設定
iInputTexture->SetResource( iTextureBuffer[1] );
iEffectModel->GetTechniqueByName( "Main" )->GetPassByIndex(0)
      ->Apply(0); // ←できない, (B)でBusy

// 出力の設定
iDevice->OMSetRenderTargets( 1, &iRenderBuffer[0], NULL );

このとき、Resource[1] は前のレンダリングで RenderTarget として
登録されています。すでに Busy なので、入力テクスチャとして設定する
ことができません。
(ちなみにこれらの衝突は正確には Draw の実行タイミングで検出されます)

衝突を回避するには、先に RenderTarget のステートをクリアしておくか
別のターゲットを登録して参照をはずす必要があります。

// 入力テクスチャの設定
ID3D10RenderTargetView* iRTVNull= NULL;
iDevice->OMSetRenderTargets( 1, &iRTVNull, NULL ); // 参照外しのクリア

iInputTexture->SetResource( iTextureBuffer[1] );
iEffectModel->GetTechniqueByName( "Main" )
      ->GetPassByIndex(0)->Apply(0); // ←できる

// 出力の設定
iDevice->OMSetRenderTargets( 1, &iRenderBuffer[0], NULL );

SetResource と SetRenderTarget の順番を変えても同じで、今度は
入力テクスチャとして参照されているため RenderTarget への登録が
できなくなります。

// 出力の設定
iDevice->OMSetRenderTargets( 1, &iRenderBuffer[0], NULL ); // ←できない, (A)でBusy

// 入力テクスチャの設定
iInputTexture->SetResource( iTextureBuffer[1] );
iEffectModel->GetTechniqueByName( "Main" )
      ->GetPassByIndex(0)->Apply(0);

このステート設定順の問題は以前のエントリでも触れました。
Direct3D 10 Streamと同時Resource

また ClearState() を使うとこれらのリソース参照をいっぺんにはずすことができます。
Direct3D 10 ClearState
ただし必要なものまで全部解除されてしまうので効率は悪くなります。

●本題

Effect(fx) を使って描画していると、このリソースの衝突がどうしても
発生してしまうことがあります。C/C++ のプログラムコード上は正しくても
うまくいかず、それだけでは原因がわからないのです。

// list2
iInputTexture->SetResource( iTextureBuffer[0] ); // (C)
iEffectModel->GetTechniqueByName( "Update" )
      ->GetPassByIndex(0)->Apply(0);

 ~描画など

// 入力テクスチャの設定
iInputTexture->SetResource( iTextureBuffer[1] ); // (D)
iEffectModel->GetTechniqueByName( "Main" )
      ->GetPassByIndex(0)->Apply(0);

// 出力の設定
iDevice->OMSetRenderTargets( 1, &iRenderBuffer[0], NULL ); // (E)

list2 の (C) で Resource[0] を入力テクスチャとして参照し、一旦描画
しています。その後 (D) でテクスチャを変更して、別の Technique を使って
描画しようとしています。

InputTexture は iTextureBuffer[1] (Resource[1]) で置換されているし
Apply() もしているので、(E) のタイミングでは Resource[0] はフリーに
なったように見えます。

ところがシェーダーによっては (E) の SetRenderTarget() で衝突してしまう
可能性があります。

重要なのは描画に使っている Technique が異なっているということです。

ID3D10Effect 上では同一の変数&インターフェースとしてリソース登録が
できるものの、書き換えられる場所はシェーダーによって違うからです。

(1) シェーダーリソースの登録は、VS, GS, PS それぞれ別管理となっている

“Update” のシェーダーでは、InputTexture を VS と PS の両方で参照
しているかもしれません。Core API での Resource 登録は VS, GS, PS
それぞれ別なので、下記のように専用の登録 API があります。

ID3D10Device::VSSetShaderResources()
ID3D10Device::GSSetShaderResources()
ID3D10Device::PSSetShaderResources()

“Main” のシェーダーで、もし InputTexture の読み込みを PS だけで
行っているとしたら、(D) の Apply() で上書きされるのは
PSSetShaderResources() だけなのです。
そのため VSSetShaderResources() の参照が残ったままとなります。

(2) シェーダーによって、各リソース参照するスロット番号が異なっている。

“Update” の PixelShader が 2枚のテクスチャを参照しているとします。
このとき InputTexture が 0 と 1 のどちらのスロットに割り当てられる
のか予測できません。(Reflection を見るとわかる)

例えば “Update” では InputTexture は Slot 1 に割り当てられており、
“Main” の PixelShader では Slot 0 だとすると、
やはり (D) の Apply() では Slot 1 の参照は上書きされずに残って
しまうのです。

HLSL コンパイラは最適化によって、各シェーダーが本当に必要としている
リソースしかバインドしません。ID3D10Effect のステート管理も最小限の
更新だけで済むように最適化が行われているので、内部の動作をある程度
把握しておかないと、このような はまり に遭遇してしまうことになります。

個別に呼び出すとシェーダー単体は問題なく動作するし、C/C++ のコードも
問題が無いので原因がなかなか見つからないかもしれません。

注意点

 ・同じ Effect/Fx でも Technique や Pass が異なっていると違うシェーダー
  であること

 ・シェーダー(Technique/Pass)ごとに Apply() しないと、厳密には各種リソース
  のステートが上書きされないこと

よくあるのはこんなケースです。

 1. 本来はシェーダーでテクスチャを参照している

 2. デバッグのために一時的に Shader を書き換えて、固定値を出力して
  動きを確認しようとする。
  (例えば PS で強制的に return float4(1,0,0,1); とするなど)

 3. このときテクスチャの参照が外れたので、Effect は Resource 設定の
  上書きを行わずに前のステートが残ってしまう。

 4. 固定値を返すように書き換えただけなのに、突然関係ないシェーダーの
  描画で衝突検出が発生して驚く

Effect(fx) を使って、高レベルな API の理解だけで済ませてしまおう・・
として、思わぬはまりに遭遇することがあります。D3D10 ではある程度
Effect 内部動作や ID3D10Device 関連のステート設定についてもきちんと
把握しておく必要があるかもしれません。