ゆとりーなの日記

日記的な事を書いて行くと思はれる

第2回〜Direct3Dの初期化〜

なんだかんだ言ってもゲームの中心はグラフィックと言われているのが現状です。早速ウィンドウに何かを描画できるようにするために今回はDirect3Dの初期化を行います。あと、今回から、"d3d9.lib"のリンクが必要になります。
DirectXのバージョンは、XPを省ると怒る人が多いのでDirectX9.cを使うことにします。また、前回のサンプルコードを実行すると、CPU使用率が半端ないことになってしまったと思いますが、そこらへんのことも今回で解消されます。
早速Direct3Dを管理するクラスを書きます。まずはインターフェイスから。

// graphicdevice.h
#pragma once
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#include <d3d9.h>
#include "comdeleter.h"

class GraphicDevice : private boost::noncopyable {
public:
    GraphicDevice(HWND window_handle, bool windowed);
    bool beginScene();
    void endScene();
private:
    D3DPRESENT_PARAMETERS present_parameters_;
    const boost::intrusive_ptr<IDirect3D9> kDirect3D_;
    const boost::intrusive_ptr<IDirect3DDevice9> kGraphicDevice_;
};

今回もコピーの挙動を定義するのが面倒なのとコピーに意義を見いだせないので、例によってboost::noncopyableを継承しておきます。
Direct3D関連のポインタですが、これ位簡素な構造であれば、デストラクタで解放してやればいいのではと思うかもしれませんが、初期化に失敗したとき思いっきりコンストラクタで例外を投げたいため、スマポにしとかないとリソースリークが起きてしまいます。スマポにはなにがよいかを考えると、参照カウンタ付きポインタを管理するのに適したboost::intrusive_ptrがよいだろうということになりました。
boost::intrusive_ptrを使うには、その型の専用参照カウンタ操作関数を書いておかなければいけません。そこで、次のようなヘッダを用意してやります。

// comdeleter.h
#pragma once
#include <cassert>
#include <unknwn.h>

// boost::intrusive_ptrを使うのに必要な参照カウンタを上げる関数
inline 
void intrusive_ptr_add_ref(IUnknown *ptr) {
    assert(ptr);
    ptr->AddRef();
}

// boost::intrusive_ptrを使うのに必要な参照カウンタを下げる関数
inline
void intrusive_ptr_release(IUnknown *ptr) {
    assert(ptr);
    ptr->Release();
}

inlineはinline展開を要請しているというよりかはむしろリンクエラー防止といった意味の方が大きいのであしからず。型をIUnknownにしてあるのは、他のCOMでも使えるようにするためです。また、ポインタのNULLチェックは不要らしいのですが、こういうときでもassertでポインタがNULLでないことを宣言しておきます。こういうのを書いておくと、コメント量が減って幸せになります。
Direct3Dを管理するクラスの実装に移ります。

// graphicdevice.cc
#include "graphicdevice.h"
#include <cassert>
#include <stdexcept>

namespace {
D3DPRESENT_PARAMETERS initPresentParameters(const HWND kWindowHandle, const bool kWindowed) {
    // ウィンドウハンドルから画面サイズ取得
    RECT client_size;
    if (!GetClientRect(kWindowHandle, &client_size)) {
        std::runtime_error("描画領域サイズの取得に失敗");
    }
    // プレゼンテーションパラメータ初期化(割と定型句)
    const D3DPRESENT_PARAMETERS kPresentParameters = {
        client_size.right,
        client_size.bottom,
        kWindowed ? D3DFMT_UNKNOWN : D3DFMT_X8R8G8B8,
        1,
        D3DMULTISAMPLE_NONE,
        0,
        D3DSWAPEFFECT_DISCARD,
        kWindowHandle,
        kWindowed ? TRUE : FALSE,
        TRUE,
        D3DFMT_D24S8,
        0,
        D3DPRESENT_RATE_DEFAULT,
        D3DPRESENT_INTERVAL_DEFAULT
    };
    return kPresentParameters;
}

IDirect3D9* createDirect3D() {
    // Direct3Dの初期化
    IDirect3D9 * const kDirect3D(Direct3DCreate9(D3D_SDK_VERSION));
    if (!kDirect3D) {
        std::runtime_error("Direct3Dの作成に失敗");
    }
    return kDirect3D;
}

IDirect3DDevice9 *createDevice(const HWND kWindowHandle, const boost::intrusive_ptr<IDirect3D9> kDirect3d, D3DPRESENT_PARAMETERS * const kPresentParameters) {
    assert(kWindowHandle);
    assert(kDirect3d);
    assert(kPresentParameters);
    IDirect3DDevice9 *graphic_device;
    // IDirect3DDevice9の初期化(割と定型句)
    if (FAILED(kDirect3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, kWindowHandle, D3DCREATE_HARDWARE_VERTEXPROCESSING, kPresentParameters, &graphic_device))) {
    	if (FAILED(kDirect3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, kWindowHandle, D3DCREATE_SOFTWARE_VERTEXPROCESSING, kPresentParameters, &graphic_device))) {
            if (FAILED(kDirect3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, kWindowHandle, D3DCREATE_SOFTWARE_VERTEXPROCESSING, kPresentParameters, &graphic_device))) {								
		std::runtime_error("Direct3Dデバイスの作成に失敗");
	    }
	}
    }
    return graphic_device;
}
}

GraphicDevice::GraphicDevice(const HWND kWindowHandle, const bool kWindowed) : present_parameters_(initPresentParameters(kWindowHandle, kWindowed)), kDirect3D_(createDirect3D()), kGraphicDevice_(createDevice(kWindowHandle, kDirect3D_, &present_parameters_), false) {
}

bool GraphicDevice::beginScene() {
    assert(kGraphicDevice_);
    // 描画開始
    if (SUCCEEDED(kGraphicDevice_->BeginScene())) {
        // 画面を黒でクリアする
	kGraphicDevice_->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER | D3DCLEAR_STENCIL, D3DCOLOR_XRGB(0, 0, 0), 1.f, 0);
	return true;
    }
    return false;
}

void GraphicDevice::endScene() {
    assert(kGraphicDevice_);
    // 描画終了
    kGraphicDevice_->EndScene();
    // 実際に画面に描画結果を表示する
    switch (kGraphicDevice_->Present(nullptr, nullptr, nullptr, nullptr)) {
    // デバイスが落っこちていた場合
    case D3DERR_DEVICELOST:
        // デバイスのリセットが出来るかを調べる
	if (kGraphicDevice_->TestCooperativeLevel() == D3DERR_DEVICENOTRESET) {
            // デバイスをリセットする
	    if (FAILED(kGraphicDevice_->Reset(&present_parameters_))) {
                throw std::runtime_error("デバイスロストからの復旧に失敗");
	    }
	}
	break;
    // ドライバエラーが起きた場合
    case D3DERR_DRIVERINTERNALERROR:
	throw std::runtime_error("内部ドライバエラーが発生");
	break;
    default:
	break;
    }
}

コンストラクタでプレゼンテーションパラメータとDirect3D関係のポインタを初期化します。各メンバの初期化に関しては、コンストラクタの初期化子で円滑に初期化できるよう値を返す無名空間関数を定義してみました。
プレゼンテーションパラメータを初期化するinitPresentParameters関数では、まずウィンドウハンドルからGetClientRect関数を使って描画領域のサイズを取得した後、割と簡単で一般的な設定で初期化しています。通常はこれで問題ないと思います。都合上値返しにしていますが、密かにコンパイラの最適化を期待していたりします。
IDirect3D9*はcreateDirect3D関数内のDirect3DCreate9関数で初期化します。これはDirectXSDK側が用意してくれているものです。
IDirect3DDevice9*を初期化するcreateDevice関数では、謎のif文ネストがありますが、これは様々な環境で初期化を成功させるために行う定石です。良い設定から順番に試していき、駄目だったらランクを落として初期化を試みます。全部だめだったら諦めるのですが、上記の3つ位の設定で試せば大抵の環境では成功してくれると思います。因みにFAILEDというのはDirectX等のCOMのメソッドが成功したかを判定してくれるマクロです。FAILEDで真なら、基本的にメソッドの内部で何か失敗があったことを表します。
Direct3Dでは、描画はIDirect3DDevice9::BeginSceneメソッドとIDirect3DDevice9::EndSceneメソッドの間で行う必要があるので、これらをラップしたメソッドも用意しておきます。
先ずはIDirect3DDevice9::BeginSceneをラップしたGraphicDevice::beginSceneメソッド。IDirect3DDevice9::BeginSceneメソッドが成功したら、画面を黒で消去するようにしておきます。
続いてIDirect3DDevice9::EndSceneをラップしたGraphicDevice::endSceneメソッドですが、まずIDirect3DDevice9::EndSceneメソッドを実行してから、描画結果を実際に画面に転送するIDirect3DDevice9::Presentメソッドを実行します。デバイスが落ちていたらこのメソッドは失敗するので、これをswitchで拾ってデバイスの復旧を試みます。D3DERR_DRIVERINTERNALERRORを拾った場合は、内部ドライバエラーなので諦めて落ちます。D3DERR_DEVICELOSTを拾った場合は、IDirect3DDevice9::TestCooperativeLevelメソッドを使って、デバイスがリセット出来るかを調べます。結果がD3DERR_DEVICENOTRESETだった場合は、リセット出来るのでIDirect3DDevice9::Resetメソッドを使ってデバイスをリセットします。このメソッドが失敗したら、これもまた深刻なエラーなので諦めて落ちます。
以上でDirect3D絡みの初期化及び描画の準備が整いました。これを前回のApplicationクラスに組み込みます。

// application.h
#pragma once
#define _WIN32_WINNT 0x0501
#include <string>
#include <boost/noncopyable.hpp>
#include <windows.h>

class GraphicDevice;

class Application : private boost::noncopyable {
public:
    Application(const std::string &caption, int client_width, int client_height, bool windowed);
    virtual ~Application();
    int run();
    void quit();
private:
    static LRESULT CALLBACK subClassProcedure(HWND window_handle, UINT message, WPARAM wp, LPARAM lp, UINT_PTR this_ptr, DWORD_PTR);
    const HWND kWindowHandle_;
    const bool kWindowed_;
};

// application.cc
#include "graphicdevice.h"

Application::Application(const std::string &caption, const int kClientWidth, const int kClientHeight, const bool kWindowed) : kWindowHandle_(initWindow(caption, kClientWidth, kClientHeight)), kWindowed_(kWindowed) {
    // ウィンドウプロシージャの関連付けを設定
    if (!SetWindowSubclass(kWindowHandle_, subClassProcedure, reinterpret_cast<UINT_PTR>(this), 0)) {
        throw std::runtime_error("ウィンドウプロシージャの関連付けに失敗");
    }
    // ウィンドウの表示
    ShowWindow(kWindowHandle_, SW_RESTORE);
}


int Application::run() {
    MSG message;
    GraphicDevice graphic_device(kWindowHandle_, kWindowed_);
    for (;;) {
        // メッセージキューにメッセージがあるかを調べる
        if (PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE)) {
            // メッセージ取得
            const BOOL kRsult(GetMessage(&message, nullptr, 0, 0));
            // 終了、あるいはエラーならループを抜ける
            if ((!kRsult) || (!(~kRsult))) {
                break;
            }
            // キー入力等のメッセージの変換
            TranslateMessage(&message);
            // メッセージの処理
            DispatchMessage(&message);
        } else {
            if (graphic_device.beginScene()) {
                // ゲームの毎フレームごとの処理
                // 描画可能
                graphic_device.endScene();
            }
        }
    }
    // メッセージループに入った後のWinMainの返り値はこれである必要がある
    return static_cast<int>(message.wParam);
}

Application::runメソッドの頭でGraphicDeviceを作り、ゲームの毎フレームごとの処理をGraphicDevice::beginSceneメソッドとGraphicDevice::endSceneメソッドで括ります。これでゲームの毎フレームごとの処理で描画が行えるようになりました。前回までのコードでは、無限ループを延々と実行していたため、CPU使用率が残念なことになっていましたが、今回の追加でDirect3Dがリフレッシュレートに合わせていい感じに休んでくれるようになるため、CPU使用率も大分落ち着きます。