カウンタ

  デバイスの消失

Google
▲探し物はこちら

 Direct3D のデバイスは常に正常に動作するという保障はなく、なんらかの条件により、デバイスが使えなくなってしまう状況ができることがあります。これらの要因は様々であり、大まかに言えば下のような状況下で発生することが多いです。

  • 自分がフルスクリーンの時に他のウインドウがアクティブになったとき
  • 画面モードを変更したとき(ウインドウモード⇔フルスクリーン
  • スクリーンセーバーが起動したとき
  • PCがスリープモードに移行したとき

 などのような、画面全体が遷移する場合に多く見られます。特に自動起動アプリがアクティブになったりとか、スクリーンセーバーが起動するなどは意外と発生しやすいので、これにきちんと対処しないと基本的に「強制終了」という形になってしまうのがほとんどです。

 なので今回は、そのような状況になったとしてもバックグラウンドで正常に状態を保てるように、デバイスの状態を確認するプログラムを組み込んでみます。

デバイスの消失

 下のリンクから今回のプロジェクトをダウンロードできます。

ファイル名 言語 サイズ バージョン
devicelost_cs_1_1.zip C# 28KB 1.1
devicelost_vb_1_1.zip VB.NET 37KB 1.1
devicelost_cpp_1_1.zip C++/CLI 17KB 1.1

 今回のメインコードファイルを載せます。重要なコードを赤色で表示させています

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>全ての初期化がOKなら true, ひとつでも失敗したら false を返すようにする</returns>
        /// <remarks>
        /// false を返した場合は、自動的にアプリケーションが終了するようになっている
        /// </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
            {
                // Direct3D デバイス作成
                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);


            // XYZライン作成
            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;
                }
            }

            // 描画内容を単色でクリアし、Zバッファもクリア
            this._device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, Color.DarkBlue, 1.0f, 0);

            // 「BeginScene」と「EndScene」の間に描画内容を記述する
            this._device.BeginScene();


            // ライトを無効
            this._device.RenderState.Lighting = false;

            // 原点に配置
            this._device.SetTransform(TransformType.World, Matrix.Identity);

            // XYZラインを描画
            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。デバイスを作成する際に必須
// どのような環境でデバイスを使用するかを設定する
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;

 デバイスの消失は大抵フルスクリーンのときに起こりやすいので、今回はフルスクリーンでテストします(というよりもわざとデバイスを消失させます)。
 上記の設定は多くの環境で実行可能なように設定していますが、自分の環境で動作するように任意に設定してください。

 作成した 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」キーです。これはキーボードで別なウインドウをアクティブにする機能なのですが、この機能自体がひとつのウインドウなので、必ずデバイスをロストさせることが可能です。
 これを行うとフルスクリーンから通常の画面に戻りますので、強制終了しないかを確認してください。そして下のタスクバーからフルスクリーンアプリケーションをクリックしてもとの画面に戻るか確認してください。

タスクバー

 ただし、フルスクリーン状態で強制終了したりすると、後処理が結構面倒になる場合があるので、ウインドウモードできちんと動作するかを確認してからフルスクリーンテストを行ったほうがいいと思います。

 ちなみに今後のサンプルではデバイスのロストやリセットの処理は簡潔化のため書きません。ただし、実際にアプリケーションを作成する場合は必ずこれらの対処はするように心がけてください。

その他の関連情報です▼