2007年11月09日

メンバ関数の呼び出し規約とマルチスレッド

使い方によっては強力な効果を発揮するマルチスレッドプログラミング。Windowsプログラミングやネットワークプログラミングでは重宝する。しかし一般的にVisual C++とPlatform SDKを用いてWindowsプログラミングをする場合、コールバック関数などの設計思想がC言語主体であるので、マルチスレッドプログラミングにてクラスを用いたアプリケーションを開発する場合に不都合な点が生じる。主にスレッドを作成する関数として、CreateThread、_beginthread,_beginthreadexなどが挙げられる。しかしMSDNによると
C のランタイムライブラリに記録されている関数を使うスレッドは、CreateThread 関数と ExitThread 関数ではなく、C のランタイム関数である beginthread 関数と endthread 関数を使うべきです。この方法に従わないと、ExitThread 関数を呼び出したときにわずかなメモリリークが発生します。
と、あるので通常は_beginthreadか_beginthreadexにてスレッド作成を行うのが好ましい。
本題だがスレッドを生成するためには引数として関数のアドレスを渡す必要がある。しかしMSDNの「_beginthread、_beginthreadex (CRT)」には次のように書かれている。
start_address
新規スレッドの実行を起動するルーチンの開始アドレス。_beginthread の呼び出し規約は __cdecl または __clrcall で、_beginthreadex の呼び出し規約は __stdcall または __clrcall です。
(ちなみに__cdeclや__stdcallの意味が分からない人は検索エンジンで「呼出規約」などのキーワードで調べれば分かるだろう。)
普通のクラスのメンバ関数の呼び出し規約は__thiscallであるので、引数としては適切ではない。なので引数として渡す関数を静的メンバ関数として宣言する。そうすることにより呼び出し規約は__thiscallから__cdeclへと変更される。しかし_beginthreadex関数の引数は__stdcallか__clrcallでないといけないので、関数の宣言としてWINAPIをさらに追加することにより、呼出規約が__stdcallへと変更される。しかし問題は他にもある。それは静的メンバ関数にしてしまうと、メンバの参照が静的なものに限られてしまい、効率が悪い。そこで呼出規約を__stdcallへと変更した関数をラッパ関数の役割として使い、reinterepret_castを用いて無理やり静的でないメンバ関数の呼び出しを行えばよい。以下にプログラムの例を示す。

class Hoge{
public:
  static unsigned int WINAPI Thread(void*);
  unsigned int _Thread(void);
};

unsigned int WINAPI Hoge::Thread(void *obj)
{
  return reinterpret_cast<Hoge*>(obj)->_Thread();
}

unsigned int Hoge::_Thread(void)
{
  //通常のスレッド用プログラミング
}

/*
そして、仮にクラスのインスタンスとしてHoge obj;としている場合、
_beginthreadexの第4引数として&objを与えればよい。仮に呼び出し元が
クラスのメンバ関数であればthisを与えればよい。
*/

非常にややこしいが、これでクラスのメンバ関数を引数としたスレッドの生成が可能となる。

2007年10月05日

DirectX 9: 関数の返すHRESULT型について

DirectXのDirect3Dメソッドの戻り値にHRESULT型がある。実行した関数が何らかの原因でエラーとなった場合に、D3DERR列挙型でエラーの区分を戻り値として返す。ユーザーが定義する関数の戻り値としてHRESULT型を定義する場合などもD3DERR列挙型の定義に従って戻した方が自然であるので、以下によく用いる列挙名やマクロとそのメモをしておく。

#define FAILED(Status) ((HRESULT)(Status)<0)
#define SUCCEEDED(Status) ((HRESULT)(Status)>=0)

S_OK
→エラーは発生していない
E_FAIL
→Direcnt3D サブシステム内で原因不明のエラーが発生した
E_INVALIDARG
→無効なパラメータが関数に渡された
E_OUTOFMEMORY
→Direct3D が呼び出しを完了するための十分なメモリを割り当てることができなかった。

ところでS_OKと似た形態であるS_FALSEというものがあるが、MSDNで調べてみると、COMライブラリが既に初期化されている場合に返される値であり、成功を示している。なのでユーザー定義の関数の戻り値に勘違いでS_FALSEを返した場合に、呼び出し元でSUCCEEDEDマクロを使ってしまうと成功してしまうので注意。

2007年10月04日

DirectX 9: レンダリングパイプラインの構造

レンダリングパイプラインを構成する三要素とその役割

●頂点シェーダ
頂点バッファを入力とし、座標変換やライティングを行う。
座標変換はカメラ座標などを考慮し、最終的に出力する座標系への変換を行う。
ライティングはポリゴンに光を当てたときに、ライト、マテリアル、法線ベクトルの情報を元に、どのように色が変化するか、どのように反射をするかの計算を行う。処理後はディフューズとスペキュラーの二つの成分に分割される。ディフューズは色成分、スペキュラーは光沢成分である。

●ラスタライザ
頂点シェーダで処理された頂点データを入力とし、各頂点データから三角形を構成するデータを作成する。これをトライアングルセットアップと言う。そしてDDA(Digital Differential Analyzer)により、各頂点データを用いて三角形の内部をピクセルで塗りつぶしていく。この際にピクセル間の色を補完する仕事も行う。

●ピクセルシェーダ
テクスチャと、ラスタライザで処理されたデータをもとに、テクスチャマッピングや、アルファブレンディング、フォグなどの処理を行う。そしてレンダリングターゲット(サーフェイス)へと、出力を行う。

2007年09月02日

DirectX 9: デバイスがロストした時の復旧方法

DirectX 9にで、フルスクリーンからウィンドウへ戻ったときや、非アクティブ状態から復帰した場合に、デバイスの再構築が必要となる。

DirectX 8にて解説を行っているサイトでは、単純にReset()を呼べば良いと書かれているが、どうも上手くいかないので色々調べてみたところ、どうやら「ロストしたデバイスをリセットできない状態」というタイミングが存在するらしく、適切な関数でリセットできる状態かどうかを調べる必要があるらしい。以下にその手順を示す

if(DeviceLost){
  if(FAILED(hr = pDevice->TestCooperativeLevel())){
    if(hr == D3DERR_DEVICENOTRESET){
      //デバイスのリセットを行う処理を書く。具体的には
      //pDevice->Reset(&d3dpp)
      //を呼び出してリセットを行うが、当然d3dppはDirect3Dを
      //初期化するときに記述した形でもう一度記述を行う。
    }
  }else{
    DeviceLost  = false;
  }
}else{
  if(FAILED(pDevice->Present( NULL, NULL, NULL, NULL ))){
    DeviceLost  = true;
  }
}

まず、ロストするとPresent()が失敗するので、それをチェックする。次にTestCooperativeLevel()でデバイスをリセットできるかどうか確認を行う。もしTestCooperativeLevel()の戻り値がD3DERR_DEVICENOTRESETであれば、ロストしたデバイスの復旧が可能となり、Reset()が可能となる。