Hooking Direct3D11

Direct3D は、デバッグビルドの場合、終了時に解放されていない ID3D* 系オブジェクトがあるとその旨を警告してくれます。しかし、その時出してくれる情報は、どの class にいくつ開放漏れがあります、というだけでデバッグの助けにはあまりなりません。
これがものすごく不満だったので、もっと親切なリークチェッカを作ってみました。


https://github.com/i-saint/D3DHookInterface/tree/master/LeakChecker (一括ダウンロード)


使い方は、DirectX の Device & SwapChain を作成した後 D3D11LeakCheckerInitialize() を呼び、リークチェックしたい箇所で D3D11LeakCheckerPrintLeakInfo() を呼ぶだけです。その時点で解放されていないリソースの、作成時のコールスタック、AddRef() / Release() した時のコールスタックとその回数を表示します。
(現状 D3D11 しか対応してません)
D3D11LeakChecker


実装には、D3D の interface の vtable をすり替えて hook を仕込む、という方法を用いています。
他に、VirtualProtect() で OS の write protect を無効化して vtable を直接書き換える、import table を書き換える、などのアプローチもあることを知りましたが、1 番穏便?な方法を取りました。
実装中、DirectX の内部実装が垣間見える現象に色々遭遇して興味深かったです。以下その時のメモ。

  • DirectX の interface のメンバ関数は stdcall
    • これに気づかず、x86 版で第 1 引数に this が来るという現象が起きて悩みました。(x64 だと calling convention は統一されてるのでこの問題は起きない) そもそもメンバ関数に thiscall 以外を指定できることをこれで初めて知りました。
    • DirectX には C 用の interface も用意されているので、thiscall だと実装上都合がよくないのかもしれません。
  • DirectX は Release ビルドでは自ら vtable 書き換えを行なっている
    • Release ビルドの場合、IDXGISwapChain::Present() の中で ID3D11DeviceContext の描画系メンバ関数の書き換えが行われているようです。最適化のためと推測されます。 (該当箇所 http://codepad.org/1eqo7XQZ )
    • vtable を直接書き換えるアプローチで hook を実装してたら、IDXGISwapChain::Present() するたびに vtable が元に戻っていてナニゴトかと思いました。
  • ID3D11Device::CreateSamplerState() は実際に作成を行うとは限らない
    • 過去に同じパラメータで作成されたものがあると、参照カウンタだけ上げてそれを返すようです。ありそうな最適化ではありますが、意表をつかれました。(Create を名乗ってるのに Create してない!!)
    • 未確認ですがたぶん CreateSamplerState() の他にもいくつかあると思います。
  • IUnknown::Release() が 0 を返す時、その時点では実際の開放は行われていない
    • どうも排他制御して削除してねフラグを立てるだけのようで、実際の開放は全く別のタイミングで行われているようです。描画処理が非同期に行われるのを考えるとこれも自明な気はしますが、ちょっと驚きました。
    • 既に参照カウンタが 0 になってるオブジェクトに Release() した時、即座にクラッシュするケースとそうはならないケースを経験した覚えがありますが、これが原因なのかもしれません。
  • vtable すり替えをやってるときに NVIDIA Nsight でデバッグ実行するとクラッシュする
    • Nsight でデバッグ実行すると、D3D の API 呼び出しの時 Nsight のモジュールが色々コールスタックに挟まってるので、Nsight も色々ねじ込んでて競合してそうな感じです。結局 Nsight の dll を検出したら hook しないようにして回避しました。
    • プロファイル系ツールや動画撮影系ツールはこういうお行儀の悪いことをしないと実装できないことが多そうです。