syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

Effects11 for Windowsストア アプリ

(これは2013-04-13に書いた故OCNブログの記事を加筆修正したものです)

今回は旧D3DXライブラリの歴史にちょっとだけ触れるので、前置きが長いです。

Windowsストア アプリを開発できるVisual Studio 2012用のDirectX SDKWindows SDK 8.0)では、これまでDirect3D開発者にとっておなじみだったD3DXライブラリがほぼ完全に廃止されています。D3DXのピークはDirect3D 9用のD3DX9で、数学関数などのルーチン系だけでなくフォントやスプライト、エフェクト フレームワーク(Effects9)、果てはスキンメッシュやアニメーション コントローラー、GI(PRT)用のSH計算関数までデフォルトで用意されている充実っぷりでした。時は流れ、プログラマブル シェーダーが必須となったDirect3D 10では、D3DX10が大幅に弱体化し、スキンメッシュやアニメーション コントローラーがなくなった代わりに、エフェクト フレームワーク(Effects10)がコアAPIに組み込まれました。しかし続くDirect3D 11ではD3DX11がさらに弱体化し、ついにエフェクトもビルド済みライブラリの形では提供されなくなり、スタティックライブラリ用のソースコード(Effects11)のみがDirectX SDKのサンプルとして付属するだけになりました。

デスクトップ アプリの開発では、Visual Studio 2012/2013において、Visual C++ 2010コンパイラWindows SDK 7.x+DirectX SDK June 2010などの組み合わせを引き続き使用するように設定することもできます(ただしWindows SDK 7.xを使用する場合、Windows SDK 8.0のDirectX 11.1やDirect2D 1.1は使えません。ツールセット設定やディレクトリ優先順位を調整して、D3DコアライブラリはWindows SDK 8.0/8.1を使い、DirectX SDK June 2010のD3DXのみ補助的に使う、ということはできます)。しかし、Windows 8/RT向けストア アプリの開発ではVS 2012とWindows SDK 8.0の組み合わせが必須となっており、またD3DXランタイムおよびD3DCompilerランタイムが一切使用できません(これらのランタイムをリンクしてしまうとアプリが起動不能になる)。HLSLの実行時コンパイルをストア アプリでサポートしない理由などは下記で言及されてます。

ちなみにNVIDIA C for Graphics(Cg)のランタイムは、実はToolkitバージョン3.1時点でD3DXとD3DCompilerのランタイムに依存しているため、Cgランタイムを使ったストア アプリは現時点では作れません。

代わりにストア アプリでのDirectX 11補助ライブラリとして、CodePlexDirectX Tool Kit(DirectXTK, DXTK)というのが公開されているのですが、こいつは(少なくとも自分にとっては)なんとも使いにくいライブラリになってしまっています。従来のエフェクト フレームワークでは、エフェクトファイル(.fxファイル)に複数のシェーダー関数やステートを記述して、組み合わせをpassとtechniqueの形で階層化しパイプラインを構築するということが簡単にできていたのですが、DXTKのEffectはC++コードでパイプラインをガリガリ組み立てていくという、なんとも原始的なことをやってます。最初から用意されている出来合いの基本シェーダーだけを使うのであればそれでもいいかもしれませんが、試行錯誤しながらパイプラインをガンガン書き直す場合は辛いです。なにより今までのエフェクトファイルの資産がほとんど使えず、エフェクトのサブセットフォーマットすらサポートされないので、パイプラインおよびシェーダーステージごとに分割していかなければならないのはかなりの苦痛。

そこで自分が考えたのは、

「Effects11はソース提供されてるんだから、ストア アプリで使えるように修正したらいいじゃない!!」

結論から言うと、わりと簡単にできます。ただしストア アプリ対応のためにD3DCompilerランタイムへの依存を断つといくつかの制限が発生するため、エフェクト フレームワークのリフレクション機能をフル活用しているようなプログラムに対してはコード修正が必要です。が、エフェクトファイルによるパイプラインの構築という最大のメリットがそのまま活かせるのは大きいでしょう。エフェクトファイルではcs_5_0のようなDirectX 11対応H/W用プロファイルだけでなく、vs_4_0_level_9_1やgs_4_0のようなダウンレベルH/W用のプロファイルも使えるので、Surface RT向けアプリ(ARM用アプリ)の開発にも使えるはずです。またD3DCompilerへの依存を断つことで、DirectXエンドユーザーランタイムをインストールする必要がなくなるため、従来のWindows 7向けデスクトップ アプリでこの制限付きEffects11を心置きなく使えるというメリットもあります。

%ProgramFiles(x86)%\Microsoft DirectX SDK (June 2010)\Documentation\License Agreements\DirectX SDK EULA.txt

を読むかぎり、Effects11に関しては改変・再頒布して良いらしいので、ストア アプリでも使えるように修正したCompact Effects11と、そのCompact Effects11を使ったストア アプリのサンプルをアップしておきます。利用は自己責任で。サンプルのほうはぶっちゃけVisual Studio 2012のDirect3DアプリのテンプレートをEffects11使って手直ししただけですが、ID3DX11EffectTechniqueは普通に使えることが実証されました。

なお、サンプルを見れば分かりますが、特に大きな制限として、

(1) D3DCompilerランタイムを使用した、エフェクトの実行時コンパイルができない
(2) 頂点シェーダーBLOBのリフレクションが効かない
(3) 名前によるエフェクト変数(シェーダー定数)の参照と更新はできない
(4) ステート構文によるSamplerStateの設定はできない
(5) 動的シェーダーリンク(Dynamic Shader Linkage)はローレベルAPIで対処

などがあります。

(1) はD3DX11CompileFromFile()が使えないということですが、コンパイルVisual Studioのビルドプロセスで行なって、実行時にコンパイル済みバイトコードをD3DX11CreateEffectFromMemory()に食わせてやることで対処します。VS 2012/2013であれば.fxファイルや.hlslファイルの単純なコンパイル設定はVisual Studio IDEに専用のツールが組み込まれているので簡単ですが(VC++のプロパティ設定UIが使える)、以前のバージョンのVisual Studioでデスクトップ アプリを開発する場合や、1つのファイルに対してオプションの異なる複数のコンパイル処理を走らせたりする場合はコマンドラインでfxc.exeを使って別途エフェクトファイルをコンパイルするか、カスタムビルドツール設定でのコマンドライン指定が要ります(※後述)。

(2) はID3DX11EffectPass::GetDesc()によるバイトコードの取得が使えないということですが、頂点シェーダーバイトコードのバイナリデータがないとID3D11Device::CreateInputLayout()による頂点入力レイアウトの作成ができません。エフェクトファイル本体のコンパイルとは別に頂点シェーダーだけエントリーポイント関数名を指定してコンパイルし、バイトコードを事前作成することで対処します。これも(1)同様です。

(3) はID3DX11EffectVectorVariable::SetFloatVector()などが一律使えないということですが、代わりにcbufferを使って、C++による明示的な定数バッファの作成と更新を行なうことで対処します。サンプラー変数やテクスチャなどのリソース変数(ID3DX11EffectSamplerVariable, ID3DX11EffectShaderResourceVariable)に関しても同様です。まぁヘタにエフェクト変数に頼りすぎるとDirect3Dデバッグレイヤーによるエラーレポートの解読が困難になるなどの問題をいろいろ引き起こすことがあるので、この際あきらめましょう。uniform変数ごとの名前アクセスが主流であるGLSLから移植したりする際はこのあたりに注意が必要です(OpenGL 3.1やES 3.0では逆に定数バッファ相当機能のUBOが追加されているので、cbufferとUBOを使えばDirect3D/OpenGL間でデータ構造の共有もしやすくなります)。

(4) はエフェクトファイルでなくC++でステートオブジェクトを作成・設定することで対処します。なお、サンプラーステートはC++で明示的作成する必要がありますが、レンダリングステートは従来通りエフェクトファイル側で書けます。

(5) はパフォーマンス損失を抑えつつシェーダーの組み合わせ爆発を防ぐために導入されたDirectX 11(シェーダーモデル5.0)の新機能で、シェーダープログラムでinterface(仮想メソッド)が使えるというとんでもない代物なのですが、(3)同様にID3DX11EffectInterfaceVariable::SetClassInstance()などはD3DCompilerのリフレクション(D3DReflect()関数)に依存しているため使えません。そのため、インスタンス テーブル用の定数バッファを明示的に宣言したり、ID3D11DeviceContext::XXSetShader()メソッドに対して明示的にID3D11ClassInstance配列を渡したり、といった工夫が必要になります(ちなみに動的シェーダーリンク目的以外でも、HLSLでclassキーワードを使って単純なクラスを定義することはできます)。ID3DX11Effect::GetClassLinkage()やID3D11ClassLinkage::GetClassInstance()に関しては普通に使えるようなので、Effects11を一切使わない場合と比べて多少は楽になる部分もありますが、ID3DX11EffectPass::Apply()はID3D11ClassInstanceを受け取れないのでやはりトリックが必要になります。実は、

// FX 側:
PixelShader g_psHyperShader = CompileShader(ps_5_0, psHyperShader());
// C++ 側:
m_pEffect->GetVariableByName("g_psHyperShader")->AsShader()->GetPixelShader(0, &pPixelShader);

という感じで特定のシェーダーステージのインターフェイスを取得する機能は動作するので、いったん動的シェーダーリンクを使わないダミーシェーダーを含んだエフェクトのパスを適用した後、特定のシェーダーステージだけ後から設定し直すということはできるんですが、複数のインターフェイスを使う場合は結局ID3D11ShaderReflection::GetNumInterfaceSlots()やID3D11ShaderReflectionVariable::GetInterfaceSlot()を使ってリフレクション経由でインターフェイス スロット情報を調べる必要があるため、動的シェーダーリンクを使うシェーダーだけはエフェクトとは別にコンパイルするようにしたほうが良いでしょう。

長々と書きましたが、D3DCompilerランタイムが使えないことによる制約はかなり大きいです。エフェクト フレームワークのほとんどの便利機能が使えなくなります。ただDXTKのEffectを使うよりは生産性が高くなることは間違いないし、これまでのエフェクトファイルの資産がある程度使えるのはありがたいです。

DirectX 12ではシェーダーステージの追加だけでなく、シェーダー自体の在り方もまたガラッと変わる可能性はあるので、Effects11を更新してEffects12に、ってわけにはいかないと思います。そもそも新しいD3DCompilerがリリースされても、DirectX 12対応のエフェクトファイル仕様(fx_6_0?, technique12など)はもう現れないでしょう。ただストア アプリの場合、DirectX 11.1/Direct2D 1.1や、DirectX 11.2/Direct2D 1.2テクノロジーにOSレベルでがっつり依存しているフレームワーク(&サンドボックス)内でしか動作できないため、DirectX 12がリリースされてもすぐに対応できるわけじゃないと思います(DirectX 12はXbox Oneにはプラットフォーム更新プログラムとして必ず供給されるでしょうが、Windows 8.xや7にバックポートされる可能性は低く、当然DirectX 12対応のストア アプリ/デスクトップ アプリはWindows 10でしか動作しないように思われます)。

ちなみにコンピュート シェーダー用のエクステンションであるD3DCSX拡張ライブラリは一応D3DXの仲間なのですが、例外的にWindows SDK 8.0/8.1にも同梱されています。ただし、これを使うとd3dcsx_46.dll/d3dcsx_47.dllに依存してしまうことに注意が必要。また、ストア アプリでは使えません。

カスタム ビルド ツールによるシェーダーコンパイル

カスタム ビルド ツールを指定するとき、例えば.fxファイルに対して下記のような「コマンド ライン」を指定します。

@echo off
setlocal EnableDelayedExpansion
echo Now compiling effect file by strict mode...
set varDefs=/D DOWN_SAMPLED_TEX_SIZE=128 /D COMPUTING_TEMP_WORK_SIZE=256
"%DXSDK_DIR%Utilities\bin\x86\fxc.exe" /nologo /Ges MyHLSL\MyShader.fx /Fo "$(OutDir)MyShader.fxbin" /T fx_5_0 %varDefs%
if "!ERRORLEVEL!"  neq "0" ( exit !ERRORLEVEL! )
"%DXSDK_DIR%Utilities\bin\x86\fxc.exe" /nologo /Ges MyHLSL\VSInOutStruct.hlsli /Fo "$(OutDir)MyVertexXX.vsbin" /E vsDummyLayoutXX /T vs_4_0_level_9_1
if "!ERRORLEVEL!"  neq "0" ( exit !ERRORLEVEL! )
endlocal

各オプションの意味は、fxc.exeのヘルプに書いてあります。

VS 2012/2013の場合は起動するだけで「%ProgramFiles(x86)%\Windows Kits\8.0\bin\x86」もしくは「%ProgramFiles(x86)%\Windows Kits\8.1\bin\x86」へのパスが通っているので、上記の「%DXSDK_DIR%Utilities\bin\x86\...」というパス指定は不要となり、「fxc.exe /nologo ...」でOKです(以前のバージョンのDirectX SDKのインストールも不要です)。

注意点としては、fxc.exeを実行するたびにERRORLEVELをちゃんとチェックすること、入出力に相対パスを指定するときはVC++プロジェクト ファイル(.vcproj)からの相対パスを記述すること。また、今回の例でいうと、「出力ファイル」に「$(OutDir)MyShader.fxbin」を、「追加の依存ファイル」に「MyHLSL\VSInOutStruct.hlsli;」を指定しておくと、ソースファイル.fxやヘッダーファイル.hlsliの変更、プライマリー出力ファイル.fxbinの不在を自動検出することができるようになります。

なお、「構成プロパティ」→「全般」→「クリーン時に削除する拡張子」に、*.fxbinや*.vsbinを指定しておくとさらに確実になるかもしれません。上記例の.fxbinや.vsbinは自分が勝手に付けたシェーダーバイトコード用拡張子です。VS 2012/2013の組み込みビルド ツールでは、シェーダーバイトコードの拡張子は.cso(Compiled Shader Object?)がデフォルトのようですが、あえて区別を付けるために分けています。

ちなみにVS 2012/2013でHLSLのシンタックス ハイライト(色分け)が効くようになる拡張子は、.fx, .hlsl, .hlsliだけのようで、「ツール」→「オプション」→「テキスト エディター」→「ファイル拡張子」で追加することもできない模様。

なお、自分はCg(.cg, .cgfx)、OpenCL(.cl)、GLSL(.vert, .frag, .glsl)のソースファイルをVisual Studioで編集する際はVC++エディタに関連付けていますが、float4やvec4などが色分けされないとちょっと不便。IDEの設定ファイルをいじると、ユーザー定義型をキーワードとして色分けすることもできるようになるんですが、わざわざそこまでするのも面倒……どのみちインテリセンスは効かないので、愛用のテキストエディタであるMIFESで編集したほうがよかったりします。

Windows 8.1のD3DCompiler

なおWindows 8.1DirectX 11.2では分割コンパイルのサポートに伴って、どうやらD3DCompilerのAPIの一部がストア アプリでもエンドユーザー環境で再び使えるようになっているらしいです。

復活した詳しい経緯は調べてませんが、たぶんDirectXのパワーユーザーである海外のハイエンドゲーム開発者から実行時コンパイルに関する強い要望もあったんだと思います(現にCrysisとかはシェーダーの実行時コンパイルを使って実行環境や設定に合わせて最適化してるっぽい)。個人的にはDynamic Shader Linkage用のリフレクション機能は便利だとは思うけど、それ以外は特に無くても困らないと思うんですが。また、HLSL shader compiler sampleなるものも公開されています。DirectX 11.2を採用しているXbox Oneでも同様にD3DCompilerが使えるようになる模様。まだ試してはいませんが、つまりWindows 8.1Xbox OneではVisual Studio 2013/Windows SDK 8.1に対応する最新のD3DCompilerランタイムが標準インストールされていて、デスクトップ アプリ同様にDynamic Shader LinkageはもちろんEffects11の全機能が使えるってことでしょう。ただ、Win7Win8.0などの旧環境でも(D3DCompilerランタイムの追加インストールをすることなく)そのまま動作するようにしたければ、今回紹介したCompact Effects11を使う意義はありそうです。

オリジナルのEffects11に関して

今回ベースにしたEffects11は、DirectX SDK June 2010に付属していたソースコード+プロジェクトです。なお、Effects11自体は一応CodePlexに移管されているようです。ただしD3DCompilerに依存する従来のコードをそのまま使っているらしく、Win8.0ストアアプリには使えない模様(ストアに提出できない)。いずれにせよ、将来的にDirectX 12のシェーダーモデル6.0対応fxc.exeではEffects12やエフェクトファイルの新しいプロファイル(fx_6_0?)やテクニック(technique12)が提供されない可能性が高いので、それを覚悟して使う必要があります。