ホーム < ゲームつくろー! < DirectX10技術編

その8 ステートオブジェクトは複製して良いの?


 Direct3D10で地味ですが「うわーどうするかなぁ…」と悩んでしまう問題があります。それが表題にもある「ステートオブジェクト」。例えばDirectX9の時、ブレンドステートでアルファブレンディングをする時には次のように設定していました:

DirectX9でのアルファブレンディング設定
device->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE );
device->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA );
device->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );
device->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );
device->SetRenderState( D3DRS_BLENDOP, D3DBLENDOP_ADD );

「アルファブレンドを有効にして、Src色はSrc.aを、Dest色は(1-Src.a)を乗算し、それらを足し算する」という設定をしています。同様の事をDirect3D10で行うには次のように記述する必要があります:

Direct3D10でのステートブレンド設定
D3D10_BLEND_DESC blDesc;
memset( &blDesc, 0, sizeof( blDesc ) );
blDesc.BlendEnable[ 0 ] = TRUE;
blDesc.SrcBlend = D3D10_BLEND_SRC_ALPHA;
blDesc.DestBlend = D3D10_BLEND_INV_SRC_ALPHA;
blDesc.BlendOp = D3D10_BLEND_OP_ADD;
blDesc.SrcBlendAlpha = D3D10_BLEND_SRC_ALPHA;
blDesc.DestBlendAlpha = D3D10_BLEND_INV_SRC_ALPHA;
blDesc.BlendOpAlpha = D3D10_BLEND_OP_ADD;
blDesc.RenderTargetWriteMask[ 0 ] = D3D10_COLOR_WRITE_ENABLE_ALL;

ID3D10BlendState *blState = 0;
cpDevice_->CreateBlendState( &blDesc, &blState );

cpDevice_->OMSetBlendState( blState, D3DXVECTOR4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xffffffff );

 Direct3D10ではブレンドステートはひとまとまりとなってオブジェクト化されるようになりました。ここはDirectX9から大きく変わった部分です。オブジェクトを作るためにD3D10_BLEND_DESC構造体にパラメータを設定し、ID3D10Device::CreateBlendStateメソッドに渡すとブレンドステートオブジェクト(ID3D10BlendState)が作成されます。描画時に作成したブレンドステートをID3D10Device::OMSetBlendStateメソッドに渡すとそのステートが描画リストに積まれます。

 ステートがオブジェクト化される事により管理上で幾つか疑問が出てくるようになります。例えば、設定してPresent(描画処理開始)する前にステートオブジェクトを削除しても良いのでしょうか?つまり「ステートの寿命問題」です。またオブジェクトを作るという事は作成コストがかかります。全く同じステートがあったとして、CreateBlendStateメソッドのような生成メソッドを都度呼ぶべきでしょうか?これは「ステート複製問題」です。他にも一つのモデルを描画する時に、全ステートを都度設定するべきなのでしょうか?直前に同じステートを設定している時にそれは引き継がれるのか?これは「ステート重複設定問題」です(これはオブジェクト化されなくても問題ではありますが)。

 このようにステートがオブジェクト化される事により湧き上がる諸問題。果たして答えはいかに?一つずつ検証してみましょう。



@ ステート寿命問題

 作成したステートオブジェクトは描画デバイスに設定され、Present時にその情報が使われます。果たしてこの描画プロセスでステートオブジェクトの寿命はどのように変化しているのでしょうか?またどのタイミングで削除して良いのでしょうか?

 そもそもなぜこれを気にするかというと、描画処理のマルチスレッド化で大きくかかわってくるからです。例えば以下の状況を考えてみて下さい:

 Thread 0で更新処理をガンガン回しているとします。あるStateオブジェクトを作成し、それを描画デバイスに設定しました。続いてPresentを指示し描画を開始します。その描画処理は別スレッドで回しているとしましょう。Present指示でThread 1では直ちに描画処理が始められます。と同時にThread0では次の更新処理が回り出します。並列処理が起きているわけです。その更新で先に作成したステートがReleaseされたとしましょう。その時Present側でそのステートの情報にまだたどり着いていないとした時、果たしてそのステート情報は残っている(正常に動作する)のでしょうか?

 もしこの辺りがセンシティブだとしたら、ステートのリリースタイミングは相当に気を付ける必要があります。これを確かめるために、まずは作成、設定時のステートオブジェクトの参照カウンタをチェックしてみましょう:

作成、設定時のステートオブジェクトの参照カウンタ数
// 生成
ID3D10BlendState *blState = 0;
device_->CreateBlendState( &blDesc, &blState );
uint32_t refCount_Create = blState->AddRef() - 1;
blState->Release();

// 設定
device_->OMSetBlendState( blState, D3DXVECTOR4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xffffffff );
uint32_t refCount_Set = blState->AddRef() - 1;
blState->Release();

 ステートオブジェクトはIUnknownを継承している立派なCOMオブジェクトです。よって使用時にはAddRefメソッドでその参照カウンタを増やす義務があります。AddRefメソッドは戻り値にその参照カウント数を返してくれるので、上のようにすると現在の参照カウント数を調べる事が出来ます。

 テストを回してみると、生成時の参照カウント(refCount_Create)は1でした。これは予想通り。またこれが意味する事は「生成されたステートオブジェクトを描画デバイスは共有していない」という事です。
 ではそのステートオブジェクトを描画の為に描画デバイスに設定するとどうなるか?その参照カウンタ(refCount_Set)は1でした。これは興味深い結果です。描画デバイスに設定しても描画デバイス自体はこのオブジェクトの寿命に関与しない、という事になります。

 んじゃ、描画デバイスに設定した直後にReleaseして消してしまってからPresentしても大丈夫なのか?かなり乱暴ではありますが、これをテストしてみましょう。

ステートを消してからPresent
// 生成して設定してから描画前に削除
ID3D10BlendState *blState = 0;
device_->CreateBlendState( &blDesc, &blState );
device_->OMSetBlendState( blState, D3DXVECTOR4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xffffffff );
blState->Release();

...(Draw系メソッド呼び出し)

swapChain_->Present( 1, 0 );

 作成後描画デバイスにセットした後直ちに消してしまっています。その後Draw系のメソッドを呼び、Presentメソッドを呼び出して描画を開始しています。このコード、ちゃんと動きます。実際の描画イベントはこうです:

 これはVisual Studio Graphics AnalyzerによるDirectXのイベントキャプチャの結果画面です。DrawIndexedイベントで描画パイプラインが動いています。その中のブレンドの状態を見ると、ちゃーんとブレンドの情報が反映されています(シェーダ内のブレンドステートはOFFにしてあります)。

 ここから「Direct3D10でのステート設定はステートオブジェクト内の情報がハードコピーされている」と予想されます。つまり設定さえしてしまえばその後ステートオブジェクトを削除してもその設定はちゃんと描画デバイス内に残っている、という事です。ここから、先ほどのスレッドの例もうまく動く事になります。うまい事出来ていますね(^-^)



A ステート複製問題

 ゲームで大量のオブジェクトを描画する時、多分殆どのオブジェクトに適用するステートは同じ値になります。つまりステートの重複が発生します。冒頭で見たようにDirect3D10ではステートはオブジェクトとして「生成」されます。プログラムでは何事においても生成にはコスト(メモリ量、時間)がかかります。果たしてDirect3D10で同じステートを大量に作る事は是なのでしょうか?

 これを検証するために、同じステート情報のステートオブジェクトを実際に複数作ってみましょう:

同じ情報のステートを複数生成
// ブレンドステート
D3D10_BLEND_DESC blDesc;
memset( &blDesc, 0, sizeof( blDesc ) );
blDesc.BlendEnable[ 0 ] = TRUE;
blDesc.SrcBlend = D3D10_BLEND_SRC_ALPHA;
blDesc.DestBlend = D3D10_BLEND_INV_SRC_ALPHA;
blDesc.BlendOp = D3D10_BLEND_OP_ADD;
blDesc.SrcBlendAlpha = D3D10_BLEND_SRC_ALPHA;
blDesc.DestBlendAlpha = D3D10_BLEND_INV_SRC_ALPHA;
blDesc.BlendOpAlpha = D3D10_BLEND_OP_ADD;
blDesc.RenderTargetWriteMask[ 0 ] = D3D10_COLOR_WRITE_ENABLE_ALL;

// 複数生成
ID3D10BlendState *blState0, *blState1 = 0;
device_->CreateBlendState( &blDesc, &blState0 );
device_->CreateBlendState( &blDesc, &blState1 );
void *p0 = blState0;
void *p1 = blState1;

 同じblDescで2つのブレンドステートを作成しています。その後にそのポインタを格納しています。

 実際に試してみて「おお!」となったのですが、大変すばらしい事に「p0とp1は同じポインタ値」になります。そしてブレンドオブジェクトblState0の参照カウントは2になりました。これは明白に「同じステート情報の場合描画デバイスは過去に作成したステートオブジェクトを返す」というお仕事を内部でしてくれている事を示しています。

 ではちょっと意地悪をして、上でblState0を生成後Releaseした後にblState1を同じパラメータで返すとどうなるのでしょうか:

消してから再生成
ID3D10BlendState *blState0, *blState1 = 0;
device_->CreateBlendState( &blDesc, &blState0 );
void *p0 = blState0;
blState0->Release();
device_->CreateBlendState( &blDesc, &blState1 );
void *p1 = blState1;

この場合もp0とp1は同じポインタ値になりました。ちゃんと内部で保持しているんですねぇ。参照カウンタ数だと描画デバイスの内部保持はカウントしていないので、ステートオブジェクトの参照カウントは多分ウィークポインタと似たようなアルゴリズムになっているんでしょうね。

 という事で、同じパラメータのステートオブジェクトを複数作成してもメモリコスト(とそれに伴う生成コスト)は無いと考えて良さそうです。もちろん内部では「同じであるかどうか?」の判定処理は走っているでしょうから、その検索コストはあるはずです。なので不必要に作る事は最適ではないという事になりそうです。



B ステート重複設定問題

 @でステートの値は設定時にハードコピーされているらしい事がわかりました。では例えば同じステート設定の描画対象が1000個あった時に、都度ステートを設定しておいた方が良いのでしょうか?それとも最初に設定すれば後の描画でもその値は引き継がれるのでしょうか?

 これはVisual Studio Graphics Analyzerの描画イベントを見ると一目瞭然です:

 これは最初にブレンドステートを設定して2つ目のモデルを描画した時の結果です。「ブレンドの状態」タブを見ると「前のフレームで設定」という項目があり、そこに注意マークが出ています。この注意マークにカーソルを合わせると「この呼び出しは不要です。前のフレームが既にこの状態を設定しました」という注意メッセージが出てきます。ここから、一度設定したステート設定は変更しない限り描画デバイス内に残り続ける事がわかります。そして、重複設定をする事は最適な事では無い事も警告文からわかります。

 では重複設定を避けるにはどうしたら良いか?ここでAの「同じ値のステートオブジェクトは同じポインタ値になっている」という性質がとても役に立ちます。すなわち、描画ステートを設定する前に今設定されている値と比較してしまえばいいんです:

ステート設定の最適化
// 現在のステートを取得
ID3D10BlendState *curBlState = 0;
float curFactor[ 4 ];
UINT curMask = 0;
device_->OMGetBlendState( &curBlState, curFactor, &curMask );

// 設定したいステートが現在のと異なる時だけ設定
if ( !( curBlState == blState &&
        curFactor[ 0 ] == factor[ 0 ] &&
        curFactor[ 1 ] == factor[ 1 ] &&
        curFactor[ 2 ] == factor[ 2 ] &&
        curFactor[ 3 ] == factor[ 3 ] &&
        curMask == mask
) ) {
    cpDevice_->OMSetBlendState( blState, factor, mask );
}

 ブレンドステートの場合設定項目が3つあり、その内の一つFactorはfloatの配列であるため上のような比較になっています。論理積(&)にしているのは上から比較が走り、どれか一つでもはじかれたら後の比較が行われないからです。「全部等しかったらif文の中に入らない」という訳です。この判定を通してあげると:

ブレンド状態のタブ下に先ほどあった警告行が無くなりました。



C まとめ

 この章の結論を以下に列挙します:

こんな感じでしょうか。実際に比較検証してみて私も色々為になりました(^-^)

(※本章はテストのみなためサンプルはありません)