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

その52 デバイスのロストに対処する!


 DirectX9では「デバイスのロスト」という厄介な問題に対処しなければなりません。デバイスのロスト(消失)とはその名の通り描画デバイスが消失してしまう事で、一切の描画ができなくなってしまいます。それだけでなく、描画のために確保していたテクスチャやレンダリングターゲットなども使えなくなってしまいます。何も対策しなければ、ロストが起きた時点でゲームを続ける事はできなくなります。

 では、デバイスのロストに対してアプリケーションはどう対処すれば良いのか?この章ではデバイスのロストの対処について試行錯誤してみたいと思います。



@ デバイスのロストはいつ起こるのか?

 描画デバイス(IDirect3DDevice9)は通常「処理可能状態」にあります。これはいつも通りに描画できる状態の事です。しかし、例えばフルスクリーン描画の時にAlt+Tabを押したり、急にウィルスセキュリティー系の画面が前に来たり、スクリーンセーバが起動したり、現在の描画になんらかの致命傷を与える割り込みが入ると、描画デバイスは「消失状態」になり、一切の描画が失敗する状態になってしまいます。



A デバイスのロストを捉える

 デバイスのロストはどのタイミングで起きるかわかりませんが、DirectX9にはデバイスロストを捉える方法がちゃんと用意されています。普段多分一番最後に実行しているIDirect3DDevice9::Presentメソッドの戻り値をチェックします:

デバイスロストを捉える
if ( pDev->Present(0, 0, 0, 0) == D3DERR_DEVICELOST )
{
   // ロスト検知!
}

戻り値がD3DERR_DEVICELOSTだった場合、デバイスは消失状態になっていますので、特別処理をしなければなりません。



B デバイスがロストすると何が起こるのか?

 デバイスがロストすると描画が失敗してしまい画面に何も描画されなくなります。Direct3Dの他のメソッドの呼び出しは動きますが、実質何もされずに戻ってきます。さらにアプリケーションがD3DPOOL_DEFAULTフラグを指定して確保したビデオメモリリソース(テクスチャ、バッファ、レンダーターゲットなど)、デバイスが持っていたスワップチェーンがすべて無効になってしまいます。

 そのままゲームループを回していても、動きはしますがいつまでたっても描画は再開しません。描画を再開させるにはロストした描画デバイスを復活させる必要があります。



C デバイスのロストから復活する

 ロスト検知後に描画デバイスを復活させるには手順があります。

 まず、無効になってしまったD3DPOOL_DEFAULTフラグで生成したビデオメモリリソースをすべて解放します。これは一つ残らず解放する必要があります。全部解放したらIDirect3DDevice9::TestCooperativeLevelメソッドで復帰可能かをチェックします:

IDirect3DDevice9::TestCooperativeLevelメソッド
HRESULT TestCooperativeLevel(VOID);

この戻り値がD3DERR_DEVICELOSTだった場合は復帰ができません。これは完全に駄目になったという事ではなくて、描画を再開する準備が整っていない状態です。例えば画面のフォーカスが失われている時はこのフラグが返ります。気をつけたいのが、フォーカスを戻すには「Windowsのメッセージポンプ」を動作させる必要があります。ですから、

これはだめです
while( pDev->TestCooperativeLevel() == D3DERR_DEVICELOST )
   Sleep(10);

という待機はメッセージ処理が行われないので意味を成しません(私はハマリましたが(^-^;)。

 戻り値がD3DERR_DEVICENOTRESETの場合、描画デバイスを復帰できる状態にあります。その時にはIDirect3DDevice9::Resetメソッドを呼ぶことで描画デバイスが復帰します:

IDirect3DDevice9::Resetメソッド
HRESULT Reset(
   D3DPRESENT_PARAMETERS* pPresentationParameters
);

pPresentationParametersには復活させる時の描画デバイスの能力を設定するD3DPRESENT_PARAMETERS構造体を渡します。これは描画デバイスを最初に作成した時のを渡せば十分です。

 以上で描画デバイスは復帰し描画が再開されます。しかしながら、ロストした時にD3DPOOL_DEFAULTフラグで作成したビデオリソースはすべて解放して無くなっていますので、何もしなければ即効でアクセス違反が起こってしまいます。これをどうすべきなのか?そこがロストの消失の真の難しい部分になります。気合入れて行きましょう〜。



D ダブルポインタが必要になります

 ロスト後に解放したリソースは、リセットして復旧した後に直ちに作り直す必要があります。端的に言えば最初に作った時と同じように生成メソッドを呼び出して作り直せばOKです。それだけであれば、実装はそれ程難しくありません。しかし、実際の所はそう易々とは行かず大問題を解決する必要があります。

 問題点は「1つのリソースを色々な人が参照している」事実です。以下に例を示します。D3DPOOL_DEFAULTフラグを指定して作ったテクスチャがあったとしましょう。このテクスチャの実体はどこかのメモリブロックにあります。私たちは生成関数を通してそのブロックを指すポインタのみを得る事ができます。この状況を図示すると次のようになります:

 黄色い枠内が作成されたテクスチャの実体です。この先頭が0x0100にあるとします。このアドレスを格納しているのが右の緑色の枠です。この枠は0x2345という位置に確保されたとしましょう。私たちが普段CreateTextureなどから手に入れているのは緑色の枠です。

 このテクスチャへのポインタである0x0100を他の人に渡しているとします。その状態でロストが起こると、テクスチャの実体を解放しなくてはなりません。解放の瞬間の状態はこんな感じです:

テクスチャの解放はフレームワークで行うため、他の人は0x0100のテクスチャが解放されてしまった事実に気が付いてはいません。

 解放してからIDirect3DDevice9::Resetメソッドでデバイスを復旧した後、テクスチャを再度作り直します:

 問題はここです。CreateTextureなどで作り直したテクスチャは元のアドレスに配置されません。よって、前のアドレスを保持していた他の人たちは回復後に直ちに前のアドレス0x0100にアクセスしに行ってしまい、即効でメモリ保護違反が起こってしまいます。

 復旧時に他の人たちにも0x0200という新しいアドレスを教えてあげられれば良いのですが、多分それは無理でしょう。フレームワークが細部のオブジェクトをすべて管理するのは不可能です。そこで登場するのが「ダブルポインタ」です。

 ダブルポインタはポインタ変数を指すポインタです。先の例にダブルポインタを考慮した場合を図示してみます:

 青い枠がダブルポインタです。私たちが普段得る緑色の枠を指すポインタがさらにあります。テクスチャを他の人に渡す時、緑色の枠ではなくて青い枠の値である0x2345を渡すようにします。この状態で先ほどと同様にテクスチャを復旧させます。その時、0x2345アドレスに新しい0x0200を代入してしまいます。すると次のようになるわけです:

緑色のテクスチャポインタは0x0200に書き換わりましたが、0x2345は前のままであるため、他の人は自分が扱うテクスチャの変更に気が付きません。しかし、ポインタの行き着く先には新しいテクスチャがちゃんとあるため、続けて描画する事ができます。描画デバイスがロストした時、この考慮がどうしても必要になります。

 しかしながら、他の人にとっては煩わしい点が出てきます。今まで例えばm_pTexture->Lock()と呼び出せばよかったところを「(*m_pTexture)->Lock()」とする事になってしまったわけですから。しかしご安心を。こういう煩わしさを避けるため、○×で公開しているCOMポインタはダブルポインタを扱うように実装してあります。

 ○×で公開しているCOMポインタは、上のような参照先の交換をサポートしています(IKD::Com_ptr::Swapメソッド)。同じCOMポインタをコピーした人はこの交換により全員影響を受ける事になります。他の人はテクスチャインターフェイスそのものではなくてCom_ptr<IDirect3DTexutre9>の形でテクスチャを保持するようにすれば、ロスト時にNULLと交換し、復旧時に新しい復旧テクスチャと交換する事でテクスチャの交換が完了します。言葉では伝えにくい部分ではあります。この章のサンプルプログラムでしっかりと実装例を示しますので、そちらをご参照下さい。



E デバイスロスト対象リソース

 デバイスがロストした時に解放と再構築が必要なリソースをまとめます:

リソース 備考
ボリュームマップテクスチャ IDirect3DVolumeTexture9 D3DPOOL_DEFAULT指定
テクスチャ IDirect3DTexture9 D3DPOOL_DEFAULT指定
キューブマップテクスチャ IDirect3DCubeTexture9 D3DPOOL_DEFAULT指定
サーフェイス IDirect3DSurface9 D3DPOOL_DEFAULT指定
深度ステンシルリソース IDirect3DSurface9
レンダリングターゲットサーフェイス IDirect3DSurface9
頂点バッファ IDirect3DVertexBuffer9 D3DPOOL_DEFAULT指定
インデックスバッファ IDirect3DIndexBuffer9 D3DPOOL_DEFAULT指定

結構あるのですが、基本的にはテクスチャ、バッファ、サーフェイスです。これらを余す事無く再構築しなければならないわけです。幾つか実装方法があるかとは思いますが、以下の節ではクラスによるフレームワークを考えてみます。



F ロスト対応フレームワーク

 ここまでの長い説明で、ロストへの対処方法は一通り終りです。しかし、これをプログラマが意識して行うのは骨が折れます。この作業は正直ゲーム製作部分とはなんら関係ありません。こういう事を意識せずにゲームを作れるのが理想です。ですから、この作業をフレームワークが担ってくれればありがたいわけです。

 しかし、そういうフレームワークを本格的にそろえるのはかなり大変です。また説明も冗長になってしまいます。そこで、フレームワーク構築のポイントだけをピックアップすることにしました。例としてテクスチャを対象にします。

 ロストが起こった後、テクスチャは「解放」→「再構築」というプロセスを経る事になります。これはテクスチャに限らずサーフェイスや頂点バッファも同様です。そこで、ロスト対処が必要なリソースを扱うLostResourceクラスでそれらを行うメソッドを定義します:

LostResourceクラス
class LostResource
{
public:
   virtual void Backup()  = 0;    // バックアップ
   virtual void Recover( IDirect3DDevice9* pDev ) = 0;   // 復旧
};

 Backupメソッドはロストが起こった時にリソースの復旧に備えてデータをまとめたりリソース自体を解放したりするメソッドです。Recoverメソッドはロストから復旧した後にリソースを再度作り直すメソッドです。

 このベースクラスからテクスチャに特化したLostTextureクラスを派生させます:

LostTextureクラス
class LostTexture : public LostResource
{
public:
   void Regist( Com_ptr<IDirect3DTexture9> cpTex );  // テクスチャ登録
   virtual void Backup();    // バックアップ
   virtual void Recover();   // 復旧

private:
   Com_ptr<IDirect3DTexture9> m_cpTex;
   D3DSURFACE_DESC m_Desc;
};

Registクラスでロスト対象となるテクスチャをダブルポインタサポートのCOMポインタで格納します。このメソッドの実装はこんな感じです:

LostTexture::Registメソッド
void LostTexture::Regist( Com_ptr<IDirect3DTexture9> cpTex )
{
   cpTex->GetLevelDesc( 0, &m_Desc );
   m_cpTex = cpTex;
}

登録時にテクスチャの能力をIDirect3DTexture9::GetLevelDescメソッドで取得します。この情報は復旧時に必要になります。

 ロストが起こった時、Backupメソッドでは登録したテクスチャを解放します:

LostTexture:Backupメソッド
void LostTexture::Backup()
   // 登録されているテクスチャを解放する
   if( m_cpTex.GetPtr() ) {
      Com_ptr<IDirect3DTexture9> cpTmp;
      m_cpTex.Swap( cpTmp );
   }
}

ポイントはReleaseではなくてSwapを使っている所です。これでm_cpTexが持っていたテクスチャと空テクスチャが入れ替わります。この時元のテクスチャはこのメソッドを抜けた段階で自動削除されます。しかしCOMポインタ自体のコピー数はちゃんと保持されていますので、参照カウント数がおかしくなる事はありません。

 こうして破棄した後にIDirect3DDevice9::Resetメソッドでデバイスを復旧させ、その後直ちにRecoverメソッドを呼び出します:

LostTexture:Backupメソッド
void Recover( IDirect3DDevice9 *pDev ) // リカバー
{
   // テクスチャを復活させる
   Com_ptr<IDirect3DTexture9> cpTmp;
   D3DXCreateTexture( pDev, m_Desc.Width, m_Desc.Height, 0,
   m_Desc.Usage, m_Desc.Format, m_Desc.Pool, cpTmp.ToCreator() );

   // テクスチャを入れ替え
   m_cpTex.Swap( cpTmp );
}

cpTmpに新しいテクスチャを作成します。そしてそれをSwapメソッドで入れ替えます。m_cpTexをコピーした他の人が持つテクスチャポインタは、この段階ですべて新しいテクスチャを指すようになります。

 1つのテクスチャリソースについてはこれで復旧できます。他のリソースもこれにならってLostResourceクラスから派生させたクラスで対応すれば、同じメソッドの呼び出してすべて復旧可能です。こういうのはリストに登録するのが一番ですから、大枠のフレームワークは例えばこのようになります:

ロスト対処フレームワーク例
list<LostResource*> g_listLostResource;   // グローバルリスト


main()
{
   // プログラム色々... //

   // 作成したテクスチャをロストクラスに登録
   Com_ptr<IDirect3DTexture9> cpTex;
   D3DXCreateTexture( g_pD3DDev, 640, 480, 0, D3DUSAGE_DYNAMIC,
    D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, cpTex.ToCreator());
   LostTexture lostTexture;
   lostTexture.Regist( cpTex );
   g_listLostResource.push_back( &lostTexture );  // リストへ

   // プログラム色々... //

   // 描画
   // ロストを検知したら特別処理へ
   if ( D3DERR_DEVICELOST == g_pD3DDev->Present( 0, 0, 0, 0 ) )
   {
      // ロスト対応へジャンプ
      RecoverDevice( g_pD3DDev, d3dpp );
   }
}


// ロスト処理関数
void RecoverDevice( IDirect3DDevice9 *g_pD3DDev, D3DPRESENT_PARAMETERS &d3dpp )
{
   // リソース解放
   list<LostResource*>::iterator it;
   for( it = g_listLostResource.begin(); it != g_listLostResource.end(); it++ )
      (*it)->Backup();

   if (g_pD3DDev->TestCooperativeLevel() != D3DERR_DEVICENOTRESET )
      return; // リセット可能状態になっていない

   HRESULT hr = g_pD3DDev->Reset( &d3dpp );
   if ( hr != D3D_OK )
      PostQuitMessage(0);

   // リソース復活
   for( it = g_listLostResource.begin(); it != g_listLostResource.end(); it++ )
      (*it)->Recover( g_pD3DDev );
}


 テクスチャをLostTextureオブジェクトに登録します。ロストを検知したらRecoverDevice関数に飛び、リソースを解放します。TestCooperativeLevelメソッドでリセット可能状態をチェックして、可能ならばリセットします。リセット成功後、先のリソースを復活させます。



 他のリソースについてもLostResourceクラスの派生クラスを作成して、上のフレームワークに当てはめればロストへの対処が可能です。実際はさらにアプリケーション固有のロスト前、ロスト後処理を加えます。一度このようなフレームワークを作ってしまえば、ロストとちょっと友達になれる・・・と思います(^-^;

 このロスト動作を行うためのテストプログラムをサンプルプログラムとして挙げましたのでお試し下さい。