DirectX のコード資産

『誰も居ない』より.

DirectXはCOMでオブジェクト指向で書かれているとは言うものの、そのままではやはり使いにくい部分も多い。これは、"必要となる全てのサービスを最小限のインタフェースで提供する事"がAPIに求められる性質なので、多種多様な便利関数が含まれていないのは仕方が無い。

それを補うためにD3DXやDXUTがあるというのは事実だが、それでも不足は出る。3D用のユーティリティライブラリで満足できる物は恐らく存在しないので、必要に応じてラッパクラスを作る事になる。

今日は、頂点バッファとインデックスバッファのラッパを作っていたのだけど、実は同じ様なものを前にも1度は作った事がある。1度作っても、後になるとどうしても不備が出てきて、根本的な部分を作り直さなければいけないことがよくあるのだ。

未熟なうちは無理に完成されたラッパを作らないほうが良いのかもしれない。しかし、そうは言っても同じ様なコードを何度も書くのはやりたくない。そういう考えの衝突から泥沼に嵌ってきて、事態を打開するために元のコードを書くことを再開する。そういう悪循環から早く逃れたい。

これは自分の経験でもありますが,DirectX 開発を始めてしばらくすると,「ラッパーライブラリ作成」という作業自体に「のめり込みんでしまいやすい」という罠があります.例えば仮想関数憶えたての頃だと「Direct3DOpenGL,自前レンダラ共通のインターフェイスを作るぜ」というパターンとか.
私も昔はそういうフレームワーク作りが楽しかった時期がありましたが,変な話で,熱中している時には見えなかった共通性というものに,そういったフレームワーク作りを冷めた目で見られるようになってから気付くこともあります.当時は何でも C++ の範囲内で考えていたので,言語が思考を規定していたのかもしれません*1
以下は最近私がよく感じる DirectX の共通性ですが,いわゆる DirectX ラッパーの作り直しを繰り返している人は,こういった視点で過去の仕事の資産化が出来ているか見直してみてはどうでしょうか? 私の場合ここしばらく視点が DSL やコードの自動生成方向に向かっているので,そのテイストが強いかもしれませんけど.


リソース I/O

DirectX は昔から一貫して Lock ベースのリソース I/O を採用しています*2.実行バッファ,テクスチャ,頂点バッファ,インデクスバッファと,様々なりソースが Lock ベースで読み書きされます.DirectX のリソース I/O の特徴をいくつか挙げてみます.

  • Lock 時に予めサイズを決める必要がある.
  • パフォーマンス上の理由から,読み取り専用の Lock,書き込み専用の Lock,読み書き用の Lock が区別され,たいていの場合はリソース作成時からフラグが異なる.
  • デバイス側とアプリケーション側でメモリレイアウトの対応をとるためのスキーマが登場する.ピクセルフォーマットや,FVF,Vertex Shader Declaration など.

細かい実装 (フラグや表現) はとにかく,大まかな方向性という意味ではほとんど変化がなかったりするので,「何か前も似たような処理書いたなぁ」度が高くなりやすいです.


オブジェクト・ライフサイクル

分け方は色々あるでしょうが,私は Direct3D のデバイスを次のような状態で区別しています*3

  • 作成前
  • 作成後・BeginScene 前
  • BeginScene 後,EndScene 前
  • リセット可能なロスト状態
  • リセット不能なロスト状態
  • 終了処理前

また Win32 なウィンドウ管理では,次のようなウィンドウ状態を考慮することが多いです.

  • ウィンドウ作成前
  • ウィンドウ・アクティブ状態
  • ウィンドウ・非アクティブ状態
  • ウィンドウ・サイズ変更中
  • ウィンドウ破棄処理前

これらの状態の全組み合わせが必要になるわけではありませんが,それでも DXUT のようなフレームワークでは状態遷移に伴うイベント処理の記述がかなりの量を占めることになります.各イベント処理は単体では大したことはないのですが,高い網羅性を要求すると案外に面倒な作業です.イベントの発火がさらなる状態遷移を誘発する場合やエラー時の遷移過程をきちんと考慮しだすとコード記述量が急激に増加し,C++ 風の継承や多態だけで乗り切るにはややきついという印象があります.
ステート処理のスパゲッティと化した C++C#ソースコードは持っていても必ずしも嬉しいとは言えず,どちらかというとコーディング前のステート遷移図が残っていて助かったと感じることが多々あります.Windows Workflow Foundation (WF) の XOML データなどを保持して,C++C#ソースコードは設計から自動生成された (その場限りの) コードに過ぎないと割り切れるような時代が来るかなぁと,最近若干期待してはいるのですが.今の私なら,DXUT クラスのイベント処理をいちから書き下していると多分途中で飽きるでしょうし.

余談ですが,この「ステートによって同一メソッドの処理内容が変化する」ことは,インターフェイス指向の開発を難しくしている原因のひとつではないでしょうか.あるインターフェイス I をサポートするオブジェクト O について,I の想定するステートよりも多くのステートを O が持っていたとき,O のステートの差違が I で定義されたメソッドの動作から表面化してしまうと困ったことになります.この場合,I のヘルプだけを読んでいても問題は避けられず,結局 O のドキュメントを読むことになります.「それは設計がまずい」という話が出るでしょうが,言われて簡単に避けられるほど単純な問題であれば苦労はしないでしょう.GDNJ のIDisposable スレッドで吉松さんは「IDisposable.Dispose を正しく使うには,各実装クラスのドキュメントを読む必要がある」と主張されていましたが,そうならざるを得ない理由のひとつが,IDisposable が『破棄以前』・『破棄後』というステートに絡むインターフェイスであるためと考えられます.また,ステートは実行時のパラメータなので,コンパイル時の静的検証も難しいものとなります.
さらに,ステートの存在は IntelliSense とも相性が良くありません.IntelliSense を活用した開発スタイルでは,メソッド名と簡単な説明文だけで安易にメソッドが選択されることがありますが,これは呼出し順序が指定されている一連の API 群や,あるステートではこの API を呼び出してはいけないといったルールに対してはひどく脆弱です.
ステートに注目し,Drect3D 開発でよく見る「デバイスが初期化前に呼ばれていたらエラーを返す (NULL チェック)」や「リセット不能なロスト状態で呼ばれていたら復帰を試みる」といった処理をあちこちに散逸するという問題に AOP 的なウィービング技術を適用てみた習作が拙作 orzEngine です.まあ動機の根源には,『Essential .NET (asin:4891003685)』を読んで (当時の) Don Box にあてられていたというか,AOP 技術を使ってみたかったというのがあるわけですけど.それでも印象としては,作成するラッパクラスのメソッド数が一定数を超えてくると,属性による宣言型の実装の方が明らかに楽に記述でき,またメソッドの先頭を見るだけで処理内容が把握しやすいというメリットを感じました.属性を利用して機械的にドキュメントを生成できるというのもポイントです.最近だとエフェクトファイルのアノテーションDirectX 開発者にも割と受け入れられやすくなっているかもしれません.

*1:そして見直してみると例に漏れず [http://www.radiumsoftware.com/0603.html#060330:title=〜Manager と名前が付いてますよ]と

*2:ただし Direct3D10 では若干微調整というか,(ファイルの) メモリマップのアナロジーで Map という概念に置き換えられるでしょう

*3:これも Direct3D10 ではデバイスロストが無くなって,代わりにデバイスが抜かれた状態が追加されることで,若干変化するでしょう