デバイスの消失
Direct3D のデバイスは常に正常に動作するという保障はなく、なんらかの条件により、デバイスが使えなくなってしまう状況ができることがあります。これらの要因は様々であり、大まかに言えば下のような状況下で発生することが多いです。
- 自分がフルスクリーンの時に他のウインドウがアクティブになったとき
- 画面モードを変更したとき(ウインドウモード⇔フルスクリーン)
- スクリーンセーバーが起動したとき
- PCがスリープモードに移行したとき
などのような、画面全体が遷移する場合に多く見られます。特に自動起動アプリがアクティブになったりとか、スクリーンセーバーが起動するなどは意外と発生しやすいので、これにきちんと対処しないと基本的に「強制終了」という形になってしまうのがほとんどです。
なので今回は、そのような状況になったとしてもバックグラウンドで正常に状態を保てるように、デバイスの状態を確認するプログラムを組み込んでみます。

下のリンクから今回のプロジェクトをダウンロードできます。
今回のメインコードファイルを載せます。重要なコードを赤色で表示させています。
MainSample.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace MDXSample
{
/// <summary>
///
/// </summary>
public partial class MainSample : IDisposable
{
/// <summary>
/// スプライト
/// </summary>
private Sprite _sprite = null;
/// <summary>
/// スプライト用のテクスチャー
/// </summary>
private Texture _texture = null;
/// <summary>
/// 描画したフレーム数
/// </summary>
private int _frameCount = 0;
/// <summary>
///
/// </summary>
/// <param name="topLevelForm"></param>
/// <returns></returns>
/// <remarks>
///
/// </remarks>
public bool InitializeApplication(MainForm topLevelForm)
{
this._form = topLevelForm;
this.CreateInputEvent(topLevelForm);
// PresentParameters。デバイスを作成する際に必須
// どのような環境でデバイスを使用するかを設定する
PresentParameters pp = new PresentParameters();
// ウインドウモードなら true、フルスクリーンモードなら false を指定
pp.Windowed = false;
// スワップ効果。とりあえず「Discard」を指定。
pp.SwapEffect = SwapEffect.Discard;
// 深度ステンシルバッファ。3Dでは前後関係があるので通常 true
pp.EnableAutoDepthStencil = true;
// 自動深度ステンシル サーフェイスのフォーマット。
// 「D16」に対応しているビデオカードは多いが、前後関係の精度があまりよくない。
// できれば「D24S8」を指定したいところ。
pp.AutoDepthStencilFormat = DepthFormat.D16;
// バックバッファの幅
pp.BackBufferWidth = 640;
// バックバッファの高さ
pp.BackBufferHeight = 480;
// バックバッファのフォーマット
pp.BackBufferFormat = Format.R5G6B5;
// フルスクリーンのリフレッシュレート
pp.FullScreenRefreshRateInHz = 60;
try
{
this.CreateDevice(topLevelForm, pp);
this.CreateFont();
}
catch (DirectXException ex)
{
MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
// デバイスが消失する直前のイベント
this._device.DeviceLost += new EventHandler(this.device_DeviceLost);
// デバイスをリセットした後のイベント
this._device.DeviceReset += new EventHandler(this.device_DeviceReset);
this.CreateXYZLine();
// スプライトを作成
this._sprite = new Sprite(this._device);
// テクスチャー読み込み
this._texture = TextureLoader.FromFile(this._device, "Texture.jpg",
0, 0, 0, Usage.None, Format.Unknown, Pool.Default, Filter.Linear, Filter.Linear, 0);
return true;
}
/// <summary>
/// デバイスが消失する直前に発生
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void device_DeviceLost(object sender, EventArgs e)
{
// テクスチャーの破棄
if (this._texture != null)
{
this._texture.Dispose();
}
}
/// <summary>
/// デバイスをリセットした後に発生
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void device_DeviceReset(object sender, EventArgs e)
{
// テクスチャーの再作成
this._texture = TextureLoader.FromFile(this._device, "Texture.jpg",
0, 0, 0, Usage.None, Format.Unknown, Pool.Default, Filter.Linear, Filter.Linear, 0);
}
/// <summary>
///
/// </summary>
public void MainLoop()
{
if (this._keys[(int)Keys.Escape])
{
this._form.Close();
return;
}
this.SettingCamera();
// デバイスが動作可能かチェック
int deviceResult;
if (!this._device.CheckCooperativeLevel(out deviceResult))
{
switch ((ResultCode)deviceResult)
{
case ResultCode.DeviceLost:
// まだリセットできる状態ではないので少し待つ
System.Threading.Thread.Sleep(10);
return;
case ResultCode.DeviceNotReset:
// リセット可能状態
// デバイスをリセット
this._device.Reset(this._device.PresentationParameters);
break;
default:
// 原因不明(正確には上記以外)
// 終了させる
this._form.Close();
return;
}
}
this._device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, Color.DarkBlue, 1.0f, 0);
this._device.BeginScene();
this._device.RenderState.Lighting = false;
this._device.SetTransform(TransformType.World, Matrix.Identity);
this.RenderXYZLine();
this._sprite.Begin(SpriteFlags.None);
this._sprite.Draw(this._texture, Rectangle.Empty, Vector3.Empty,
new Vector3(200.0f, 200.0f, 0.0f), Color.White);
this._sprite.End();
this._font.DrawText(null, "[Escape]終了", 0, 0, Color.White);
this._font.DrawText(null, "θ:" + this._lensPosTheta, 0, 12, Color.White);
this._font.DrawText(null, "φ:" + this._lensPosPhi, 0, 24, Color.White);
this._font.DrawText(null, "描画フレーム数:" + this._frameCount, 0, 36, Color.White);
this._device.EndScene();
this._device.Present();
this._frameCount++;
}
/// <summary>
///
/// </summary>
public void Dispose()
{
if (this._texture != null)
{
this._texture.Dispose();
}
if (this._sprite != null)
{
this._sprite.Dispose();
}
this.DisposeResource();
}
}
}
|
では、赤文字の部分を説明していきます。MainSamplePartial.cs ファイルのコードはこちらです。
そもそも、デバイスが消失した場合、その状態を確認できれば強制終了を回避するようにプログラムを組むことは可能です。しかし、その状態のままではいつまでたってもレンダリングすることは不可能です。
そのため、デバイス消失中は常にデバイスが復元できるか確認し、可能であればデバイスを復元されるようにしなければなりません。このことは一般的に「デバイスのリセット」と呼ばれています。
デバイスをリセットすれば再びレンダリングすることは可能ですが、実際にはそれだけでは終わりません。デバイスが消失すると、そのデバイスで使用するために確保した
VRAM(ビデオメモリ)リソースも使用不可状態になってしまうのです。そのため、それらのリソースも再作成、またはリセットする必要があります。
今回はそれらも含めてコードの説明していきます。
/// <summary>
///
/// </summary>
private Sprite _sprite = null;
/// <summary>
///
/// </summary>
private Texture _texture = null;
|
手ごろなリソースとして今回はテクスチャーを使用します。スプライトはテクスチャーが正常に使用できているかを確認するために用意しています。
/// <summary>
///
/// </summary>
private int _frameCount = 0;
|
これは今回の処理とは特に関係ありません。デバイスが正常なときにだけレンダリングしているかを確認するためのカウンターです。
PresentParameters pp = new PresentParameters();
pp.Windowed = false;
pp.SwapEffect = SwapEffect.Discard;
pp.EnableAutoDepthStencil = true;
pp.AutoDepthStencilFormat = DepthFormat.D16;
pp.BackBufferWidth = 640;
pp.BackBufferHeight = 480;
pp.BackBufferFormat = Format.R5G6B5;
pp.FullScreenRefreshRateInHz = 60;
|
デバイスの消失は大抵フルスクリーンのときに起こりやすいので、今回はフルスクリーンでテストします(というよりもわざとデバイスを消失させます)。
上記の設定は多くの環境で実行可能なように設定していますが、自分の環境で動作するように任意に設定してください。
作成した PresentParameters は CreateDevice メソッドに渡しています。
this._device.DeviceLost += new EventHandler(this.device_DeviceLost);
this._device.DeviceReset += new EventHandler(this.device_DeviceReset);
|
ここでは「デバイスが消失する直前」と「デバイスをリセットした後」に処理を行うためのイベントハンドラを作成しています。後で述べますが、上記の状態になったときに特定のメソッドに処理を飛ばすことができるようになります。
蛇足ですが「デバイスが消失する直前」というのがありますが、実際には「次のフレームでデバイスが消失するぞ!」なんてのは分からないわけで、「デバイスが消失した時」でもいいような気がします。
this._sprite = new Sprite(this._device);
this._texture = TextureLoader.FromFile(this._device, "Texture.jpg",
0, 0, 0, Usage.None, Format.Unknown, Pool.Default, Filter.Linear, Filter.Linear, 0);
|
スプライトは今までどおりの作成ですが、テクスチャーはやたらと引数の多いメソッドで作成しています。実はテクスチャーはこのように細かい指定で作成することが出来るのですが、今回はこれらの説明のために使用しているわけではないので詳細は省きます。
今回重要なのは、第8引数の「Pool.Default」です。今までのテクスチャーの作成方法は実は「管理されているリソース」に分類されています。これは「Pool.Managed」を指定したのと同じで、デバイスが消失し、リセットした場合でも自動的にテクスチャーを復元してくれるというものです。
逆に「Pool.Default」を指定すると、「管理されていないリソース」になります。この場合、デバイスをリセットしたときなどに、プログラマが明示的にテクスチャーを再作成しないといけなくまります。
だったら「Pool.Managed」でいいじゃないかと思うかもしれませんが、テクスチャーをレンダリングターゲットにしたときやテクスチャーのメモリに直接書き込む場合などのような動的テクスチャーには「Pool.Managed」は使用できないので「Managed.Default」の場合も考慮しないといけません。
今回はわざと「Pool.Default」を指定しています。
|
TextureLoader.FromFile メソッド
|
| ファイルを基にしてテクスチャを作成 |
| device |
Direct3D デバイス |
| srcFile |
読み込む画像ファイル名 |
| width |
テクスチャーの幅。0のときは画像ファイルの幅を基にする |
| height |
テクスチャーの高さ。0のときは画像ファイルの高さを基にする |
| mipLevels |
ミップレベル。0の場合は1×1までの全てのミップマップチェーンを作成する。 |
| usage |
使用法。特に無ければ Usage.None |
| format |
ピクセルフォーマット。Format.Unknown の場合はファイルのフォーマットから取得 |
| pool |
メモリの配置方法。 |
| filter |
テクスチャフィルター。色の補間方法。 |
| mipFilter |
ミップマップのフィルター。色の補間方法。 |
| colorKey |
透明色。0の場合は透明色なし。 |
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void device_DeviceLost(object sender, EventArgs e)
{
if (this._texture != null)
{
this._texture.Dispose();
}
}
|
デバイスが消失したときにこのメソッドが呼ばれます。管理されていないリソースなどはここで破棄します。デバイスがロストすると Pool.Default
のテクスチャーはもう使用できない���で破棄してしまいます。
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void device_DeviceReset(object sender, EventArgs e)
{
this._texture = TextureLoader.FromFile(this._device, "Texture.jpg",
0, 0, 0, Usage.None, Format.Unknown, Pool.Default, Filter.Linear, Filter.Linear, 0);
}
|
デバイスをリセットした直後にこのメソッドが呼ばれます。管理されていないリソースはここでリセットしたり、再作成したりします。
テクスチャーにはリセットがないので、ここで再作成しています。
スプライトリソースに関してはディフォルト設定では管理されているので特に何もしません。
int deviceResult;
if (!this._device.CheckCooperativeLevel(out deviceResult))
{
switch ((ResultCode)deviceResult)
{
case ResultCode.DeviceLost:
System.Threading.Thread.Sleep(10);
return;
case ResultCode.DeviceNotReset:
this._device.Reset(this._device.PresentationParameters);
break;
default:
this._form.Close();
return;
}
}
|
メインループですが、描画関連の処理の一番最初にデバイスの状態をチェックしています。デバイスの状態は「Device.CheckCooperativeLevel」メソッドで取得することが出来ます。
Device.CheckCooperativeLevel メソッドの戻り値が true の場合は正常な状態なので特に何もしません。false
の場合は引数に結果コードが返されるので、それにしたがって処理を行います。
まず、デバイスがロストすると、基本的になんらかの結果コードが返されるので、それをチェックします。「ResultCode.DeviceLost」の時はデバイスがロストしており、かつデバイスのリセットも出来ない状態なので、Thread.Sleep メソッドで少し待つようにします。このまま描画することは出来ないので、
return でメインループを抜けています。
デバイスがリセット可能になると「ResultCode.DeviceNotReset」を取得するので、これが来たらデバイスをリセットします。リセットは「Device.Reset」メソッドを使用します。PresentParameters を渡さないといけませんが、特に変更がないのであれば、Device.PresentationParameters をそのまま渡してやっています。本来は「try-catch」できちんとリセットされたかもチェックすべきなのですが省いています。
リセットが出来れば、今までどおりレンダリングが可能になります。
それ以外のコードの場合は状況が不明なので終了するようにさせています。
this._font.DrawText(null, "描画フレーム数:" + this._frameCount, 0, 36, Color.White);
|
正常に描画しているときだけフレーム数をカウントしているので、確認用に見てください。
さて、コードの説明が終わりましたが、実行しただけではきちんと動作するのか分かりません。なので確認するためにはデバイスをわざとロストさせる必要があります。
もっとも簡単なのが「Alt + Tab」キーです。これはキーボードで別なウインドウをアクティブにする機能なのですが、この機能自体がひとつのウインドウなので、必ずデバイスをロストさせることが可能です。
これを行うとフルスクリーンから通常の画面に戻りますので、強制終了しないかを確認してください。そして下のタスクバーからフルスクリーンアプリケーションをクリックしてもとの画面に戻るか確認してください。

ただし、フルスクリーン状態で強制終了したりすると、後処理が結構面倒になる場合があるので、ウインドウモードできちんと動作するかを確認してからフルスクリーンテストを行ったほうがいいと思います。
ちなみに今後のサンプルではデバイスのロストやリセットの処理は簡潔化のため書きません。ただし、実際にアプリケーションを作成する場合は必ずこれらの対処はするように心がけてください。
|