Windows 8 移行におけるD3DX11の代替法 その1

はじめに

OSがWindows 8に変わりました。開発環境はVisual Studio 2012(VS2012))です。 以前のVisual Studio 2008からの引継ぎは、Visual Studioのデフォルトの変換で殆どの場合うまく行きますが、DirectX関連だけ問題がありました。 正確には、Visual Studioのversion違いの問題ではないのですが、D3DX11が使えなくなりました。 正確には、Windows 7時代まではDirectX関連のコンパイルにはDirectX SDKを別途インストールしてリンクしていまししたが、 Windows 8からはDirectX SDKがWindows SDKに取り込まれる形となり、別途インストールする必要がありません。 しかし、このタイミングでD3DX11がSDKに含まれなくなってしまいました。 対策自体はずいぶん前に調べたのですが、メモを残さずに忘れてしまっていました。 この度、オープンキャンパス用の古いコンテンツの再コンパイルが必要になり、同じ問題にハマって泣きそうになっています。 ので、対策を思い出しついでに、メモを残します。
このメモ時点での環境。すでにVisual Studioはインストール済みの環境を想定しています。
  1. OS:Windows 8.1 64bit
  2. C++コンパイラ:Visual Studio 2012 pro
  3. SDK: Windows SDK(特にインストール作業などはいりません)

D3DX11を使った以前のコード

Win7時代に作った以前のコードでは、D3DX11を使っているわけですが、Win8に合わせてこれを全て排除しなければいけません。 では、D3DX11の何を使っていたのか。 それを洗い出すために、とりあえずソースから以下のinclude文をコメントアウトしてd3dx11.hを外します。
#include  <d3dx11.h>
↓
//#include <d3dx11.h>
ここまででいったん再コンパイルします。そして何が問題になるか洗い出しますと、次の関数が見つからないと怒られます関数が無いと怒られます。 前者のD3DX11CompileFromFileはシェーダーファイルをコンパイルする支援関数ですね。 後者のD3DX11CreateShaderResourceViewFromFileはテクスチャーのロードなどで使う関数です。 私のコードではこの二つの関数だけをD3DX11から使っていなので、今回はとりあえずこの二つの関数の代替関数を作って対策することにします。
本ページでは、まず、D3DX11CompileFromFileをに対する回避先を記します。 D3DX11CreateShaderResourceViewFromFileに関してはまた次回に予定。

(2014/11/05追記)
コード整備の都合上、スクリーンショットを取る際に使う次のD3DX関数の代替手段から掲載しました。目次から探してください。

D3DX11CompileFromFileを使った以前の処理

まずは、D3DX11CompileFromFileの代替法を考えます。 D3DX11CompileFromFileを使った元のコードでは何をしていたのかというと、 例えば頂点シェーダに関して次の様な手続きを踏んでいました。
  1. シェーダーのソースファイル(fx)をD3DX11CompileFromFile関数でコンパイル
  2. コンパイル後のバイナリデータをID3DBlobオブジェクトとして取得
  3. コンパイル後のバイナリデータをID3D11Device::CreateVertexShader関数に渡してシェーダオブジェクトを作成
なんでソースが今更fx拡張子なんだとかそういうことは置いておいてですね、要はシェーダを実行時にコンパイルしていました。 実際のコードはどうなっていたのか見てみますと、 だいたい以下の様になっていました(web様に適当に端折って書いてます);
/* 
シェーダ―のソースファイル(TCHAR* szFileName)をから
頂点シェーダオブジェクト(ID3D11VertexShader* pVertexShader)を作成することが目的
*/
HRESULT CreateVertexShader( TCHAR* szFileName, LPCSTR szEntryPoint, LPCSTR szShaderModel, ID3D11Device* pd3dDevice, ID3D11VertexShader** resVS ){

    DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#if defined( DEBUG ) || defined( _DEBUG )
    dwShaderFlags |= D3DCOMPILE_DEBUG;
#endif

    //ソースからコンパイル//
    ID3DBlob* pVSBlob;
    ID3DBlob* pErrorBlob;
    hr = D3DX11CompileFromFile( szFileName, NULL, NULL, szEntryPoint, szShaderModel, 
        dwShaderFlags, 0, NULL, &pVSBlob, &pErrorBlob, NULL );
    if( FAILED(hr) )
    {
        if( pErrorBlob != NULL )
            OutputDebugStringA( (char*)pErrorBlob->GetBufferPointer() );
        if( pErrorBlob ) pErrorBlob->Release();
        return hr;
    }
    if( pErrorBlob ) pErrorBlob->Release();

    //この時点でコンパイル後のバイナリはpVSBlobに格納されている//
	
	// 頂点シェーダーオブジェクトの作成//
	ID3D11VertexShader* pVertexShader;
	hr = pd3dDevice->CreateVertexShader( pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), NULL, &pVertexShader );

    //成功していればpVertexShaderが出来上がっている//
    *resVS = pVertexShader;

    pVSBlob->Release();
    return hr;

}
コンパイルしていないのでバグがあるかもしれませんが(コンパイルできないからこの記事書いているんで!!)、だいたいこんな感じです。 要は、シェーダのソースファイル名(szFileName), そのエントリーポイント名(szEntryPoint), シェーダーモデルバージョン(szShaderModel)から 頂点シェーダーオブジェクトpVertexShaderを作っています。その際に、D3DX11CompileFromFileを使ってソースを実行時にコンパイルしています。

Visual Studioによるシェーダの事前コンパイル

さて、では代替策はどうするかということですが、実行時コンパイルを辞めます。 VS2012ではシェーダをVisual Studio上で事前にコンパイル出来ます(VS2008は不明)。 で、どうやるのか、以下に手順を示します。

まず、シェーダの拡張子を「hlslに変更」します。 そうしたら、そのソースをVisual Studioのプロジェクトに追加してください。 「メニュー」→「プロジェクト」→「既存の項目の追加」 でもいいですし、単純にソリューションエクスプローラーへドラッグしてもいいです。 そうしますと、あら不思議、HLSLコンパイラが使えるようになります。 どこが変わっているかというと、 「メニュー」→「プロジェクト」→「プロパティ」としてプロジェクトのプロパティを開きますと、 左のメニュー内に「HLSLコンパイラ」が追加されます(下図)。すばらしい!!

画像では全てのオプション画面を表示していますが、ここにエントリポイント名やシェーダ―モデルバージョンなんかを指定します。 また、右画面に「シェーダ―の種類」という項目がありますが、ここには頂点シェーダーやピクセルシェーダーなどの種類を、hlslファイルごとに設定してください。 ファイルごとに設定するには、このプロパティページを開いたまま、ソリューションエクスプローラー上でhlslファイルを選択すれば、 ファイルごとの設定ができます。プロパティページのタイトルバーに各hlslファイル名が表示されると思います。 この方法でファイルごとにエントリポイント名を変えることも可能ですが、全部mainで揃えてしまってもいいかと思います。 だって関数ごとにファイル分けるわけですし。
あとは「メニュー」→「ビルド」あたりからコンパイルしてやればシェーダソースがコンパイルされます。 コンパイル後に出来上がるバイナリのファイル名は"(ファイル名).cso"というものが推奨されているようですね。 ポイントとしては、以下に気を付けると楽です。 この縛りは絶対ではありませんので好みで変えてください。 ただし、以下の説明では上記の設定に従って進めます。

バイナリcsoファイルからシェーダオブジェクトの作成

シェーダーのバイナリができたので、今度はこれを基にシェーダ―オブジェクトを作ります。 ソースは以下の様になります。
/* 
シェーダ―のバイナリcsoファイル(TCHAR* csoName)をから
頂点シェーダオブジェクト(ID3D11VertexShader* pVertexShader)を作成することが目的
*/
HRESULT CreateVertexShader2( TCHAR* csoName, ID3D11Device* pd3dDevice, ID3D11VertexShader** resVS ){

    //バイナリファイルを読み込む//
	FILE* fp = _tfopen(csoName, _T("rb"));
	if( fp == NULL){
		return -1;
	}
	fseek(fp, 0, SEEK_END);
	long cso_sz = ftell(fp);
	fseek(fp, 0, SEEK_SET);
	
	unsigned char* cso_data = new unsigned char[cso_sz];
	fread(cso_data, cso_sz, 1, fp);
	fclose(fp);

    //この時点でコンパイル後のバイナリはcso_dataに格納されている//

	// 頂点シェーダーオブジェクトの作成//
	ID3D11VertexShader* pVertexShader;
	HRESULT hr = m_pd3dDevice->CreateVertexShader( cso_data, cso_sz, NULL, &pVertexShader );

    //成功していればpVertexShaderが出来上がっている//
    *resVS = pVertexShader;

	delete [] cso_data;
    return hr;

}
何をやっているかというと、単純にcsoファイルをバイナリとしてバッファに読み込んでやります。 既にコンパイルが終わっているのでD3DX11CompileFromFileを使う必要がありません。 読み込んだcsoファイルを今度はそのままCreateVertexShader関数に渡します。 ここの部分は引数の変数名を除いて古いコードと全く変わりませんね。 頂点シェーダ以外のシェーダも基本的にこの方法でシェーダオブジェクトを作れます。

これで、D3DX11CompileFromFileに関しては使用を回避できました。

余談

今回の様に事前コンパイルが嫌だって方には、D3DCompileという関数で実行時コンパイルする方法もある様です。 しかしその場合にはD3DCompiler_**.dllが配布時に必要な様子。 D3DXの廃止はまあコード書くには面倒なんだけど、配布時には「d3dx**_**.dllが無い」というユーザーからのクレームを無くせるという点で素晴らしいので、 D3DCompileを使ってしまってはこのメリットすら無くなってしまい元も子もない気がします。 私の開発しているAIScopeでは、基本OpenGL描画で3D visionとかで遊びたいマニアだけDirectX使ってくださいというスタンスなので、 OpenGLしかつかなわい時でもdllが必要ってのはちょっと。まあ、Visual Studioで遅延読み込み設定すればいいのですが、 開発者って殆どのマシンにSDK入れちゃってたりしてユーザー環境の再現は何だかんだザルなんですよね(私だけですかね)。 だからどのdllが標準で入っていないものなのか、クレームが来るまで気付かないことが良くあります。

D3DX11CreateShaderResourceViewFromFileの回避策はまた調べて次回以降に書きます。

目的だったオープンキャンパス用コンテンツは、結局古いWin7環境で直しちゃいました。 D3DXだけでなく、XAudio2までwin7~8間で互換が無いってことなので、こりゃあ一筋縄ではいかないですね。 映像・音楽関連の7用アプリの開発環境としてwin7+VS2008環境を維持しなきゃならないっぽい。しかもバーチャルマシンでなく実機で。非エコですなぁ。。。