読者です 読者をやめる 読者になる 読者になる

Direct3D Driver Model (1)

DirectX

Accurately Profiling Direct3D API Calls は,プロファイリングを正しく行うための必須知識のひとつとして,DirectX API とドライバというブラックボックスについての深い理解を挙げています.

CPU 処理は、アプリケーション処理、ランタイム処理、ドライバ処理、の 3 つに分けられます。アプリケーション処理は、プログラマの制御下にあるので、これは無視します。アプリケーションから見ると、ランタイムとドライバはブラック ボックスに見えます。なぜなら、実装されている内容をアプリケーションは制御できないからです。鍵は、ランタイムとドライバに実装されている可能性のある最適化テクニックを理解することです。これらの最適化を理解しないと、プロファイル測定に基づき、CPU が実行する処理量について安易に正しくない結論に飛び付くことになります。特に、コマンド バッファと呼ばれるものと、そのコマンド バッファがプロファイルを混乱させ得る動作に関するトピックが 2 つあります。これらのトピックは次のとおりです。

  • コマンド バッファを使ったランタイムの最適化。コマンド バッファとは、モード遷移の影響を低減するランタイムの最適化のことです。モード遷移のタイミングを制御するには、「コマンド バッファを制御する」を参照してください。
  • コマンド バッファのタイミングによる影響の無効化。モード遷移の経過時間は、プロファイルの測定に大きな影響を与える可能性があります。このような影響に対する方法としては、レンダリング シーケンスをモード遷移より大きくすることです。

DirectX Driver Model の特徴である "Command Buffer" または "Push Buffer" と呼ばれる仕組みは,普段はブラックボックスになっていてアプリケーションからは見えません.しかし,パフォーマンス測定を含む一部の状況では,このコマンドバッファの仕組みによってのみ説明される現象に出会うことがあります.今回は,このコマンドバッファについていくつか資料を紹介してみましょう.
上に引用したように,コマンドバッファとはユーザモードとカーネルモードの遷移回数を抑えるためにDirectX API 呼び出しを格納しておくバッファのことです.
上記記事では,ユーザモードからカーネルモードへの遷移を約 5000 クロック,同様にカーネルモードからユーザモードへの遷移も約 5000 クロックと見積もっています.一方,記事は DrawIndexedPrimitive の消費クロックを 1200 〜 1400 程度と見積もっています.これが意味することは,DrawIndexedPrimitive 呼び出し1回ごとにカーネルモードの遷移を行っていると,実際に必要な時間の 10 倍近くをモード遷移にとられてしまうということです.これは非常に効率の悪いことです.このようにカーネルモードへの遷移コストが大きいため,DirectX Runtime は API 呼び出しを一旦バッファに格納しておいて,一度のカーネルモード遷移でまとまったコマンドを投入しています.このときに使用されるのがコマンドバッファです*1
実際のコマンドバッファの内容はどうなっているのでしょうか? これは Windows DDK のディスプレイドライバのヘルプに記述されています.

The following figure shows portions of a sample logical command buffer. The driver's D3dDrawPrimitives2 callback receives a pointer to a command buffer in the lpDDCommands member of the D3DHAL_DRAWPRIMITIVES2DATA structure. The command buffer is always processed sequentially.

このように,コマンドバッファはオペレーションを現す D3DHAL_DP2COMMAND と,オペレーションごとの可変長データが交互に現われるバイトストリーム構造をしています.以下は D3DHAL_DP2COMMAND に含まれるオペレーションの一覧です.

typedef enum _D3DHAL_DP2OPERATION {
    D3DDP2OP_POINTS               = 1,
    D3DDP2OP_INDEXEDLINELIST      = 2,
    D3DDP2OP_INDEXEDTRIANGLELIST  = 3,
    D3DDP2OP_RENDERSTATE          = 8,
    D3DDP2OP_LINELIST             = 15,
    D3DDP2OP_LINESTRIP            = 16,
    D3DDP2OP_INDEXEDLINESTRIP     = 17,
    D3DDP2OP_TRIANGLELIST         = 18,
    D3DDP2OP_TRIANGLESTRIP        = 19,
    D3DDP2OP_INDEXEDTRIANGLESTRIP = 20,
    D3DDP2OP_TRIANGLEFAN          = 21,
    D3DDP2OP_INDEXEDTRIANGLEFAN   = 22,
    D3DDP2OP_TRIANGLEFAN_IMM      = 23,
    D3DDP2OP_LINELIST_IMM         = 24,
    D3DDP2OP_TEXTURESTAGESTATE    = 25,
    D3DDP2OP_INDEXEDTRIANGLELIST2 = 26,
    D3DDP2OP_INDEXEDLINELIST2     = 27,
    D3DDP2OP_VIEWPORTINFO         = 28,
    D3DDP2OP_WINFO                = 29,
    D3DDP2OP_SETPALETTE           = 30,
    D3DDP2OP_UPDATEPALETTE        = 31,
#if(DIRECT3D_VERSION >= 0x0700)
    D3DDP2OP_ZRANGE               = 32,
    D3DDP2OP_SETMATERIAL          = 33,
    D3DDP2OP_SETLIGHT             = 34,
    D3DDP2OP_CREATELIGHT          = 35,
    D3DDP2OP_SETTRANSFORM         = 36,
    D3DDP2OP_EXT                  = 37,
    D3DDP2OP_TEXBLT               = 38,
    D3DDP2OP_STATESET             = 39,
    D3DDP2OP_SETPRIORITY          = 40,
#endif 
    D3DDP2OP_SETRENDERTARGET      = 41,
    D3DDP2OP_CLEAR                = 42,
#if(DIRECT3D_VERSION >= 0x0700)
    D3DDP2OP_SETTEXLOD            = 43,
    D3DDP2OP_SETCLIPPLANE         = 44,
#endif 
#if(DIRECT3D_VERSION >= 0x0800)
    D3DDP2OP_CREATEVERTEXSHADER   = 45,
    D3DDP2OP_DELETEVERTEXSHADER   = 46,
    D3DDP2OP_SETVERTEXSHADER      = 47,
    D3DDP2OP_SETVERTEXSHADERCONST = 48,
    D3DDP2OP_SETSTREAMSOURCE      = 49,
    D3DDP2OP_SETSTREAMSOURCEUM    = 50,
    D3DDP2OP_SETINDICES           = 51,
    D3DDP2OP_DRAWPRIMITIVE        = 52,
    D3DDP2OP_DRAWINDEXEDPRIMITIVE = 53,
    D3DDP2OP_CREATEPIXELSHADER    = 54,
    D3DDP2OP_DELETEPIXELSHADER    = 55,
    D3DDP2OP_SETPIXELSHADER       = 56,
    D3DDP2OP_SETPIXELSHADERCONST  = 57,
    D3DDP2OP_CLIPPEDTRIANGLEFAN   = 58,
    D3DDP2OP_DRAWPRIMITIVE2       = 59,
    D3DDP2OP_DRAWINDEXEDPRIMITIVE2= 60,
    D3DDP2OP_DRAWRECTPATCH        = 61,
    D3DDP2OP_DRAWTRIPATCH         = 62,
    D3DDP2OP_VOLUMEBLT            = 63,
    D3DDP2OP_BUFFERBLT            = 64,
    D3DDP2OP_MULTIPLYTRANSFORM    = 65,
    D3DDP2OP_ADDDIRTYRECT         = 66,
    D3DDP2OP_ADDDIRTYBOX          = 67
#endif 
#if(DIRECT3D_VERSION >= 0x0900)
    D3DDP2OP_CREATEVERTEXSHADERDECL   = 71,
    D3DDP2OP_DELETEVERTEXSHADERDECL   = 72,
    D3DDP2OP_SETVERTEXSHADERDECL      = 73,
    D3DDP2OP_CREATEVERTEXSHADERFUNC   = 74,
    D3DDP2OP_DELETEVERTEXSHADERFUNC   = 75,
    D3DDP2OP_SETVERTEXSHADERFUNC      = 76,
    D3DDP2OP_SETVERTEXSHADERCONSTI    = 77,
    D3DDP2OP_SETSCISSORRECT           = 79,
    D3DDP2OP_SETSTREAMSOURCE2         = 80,
    D3DDP2OP_BLT                      = 81,
    D3DDP2OP_COLORFILL                = 82,
    D3DDP2OP_SETVERTEXSHADERCONSTB    = 83,
    D3DDP2OP_CREATEQUERY              = 84,
    D3DDP2OP_SETRENDERTARGET2         = 85,
    D3DDP2OP_SETDEPTHSTENCIL          = 86,
    D3DDP2OP_RESPONSECONTINUE         = 87,
    D3DDP2OP_RESPONSEQUERY            = 88,
    D3DDP2OP_GENERATEMIPSUBLEVELS     = 89,
    D3DDP2OP_DELETEQUERY              = 90,
    D3DDP2OP_ISSUEQUERY               = 91,
    D3DDP2OP_SETPIXELSHADERCONSTI     = 93,
    D3DDP2OP_SETPIXELSHADERCONSTB     = 94,
    D3DDP2OP_SETSTREAMSOURCEFREQ      = 95,
    D3DDP2OP_SURFACEBLT               = 96,
#endif 
} D3DHAL_DP2OPERATION;

DirectX のバージョンが上がるごとに使用可能なコマンドが増えているのは,まさに Direct3D の歴史と言えるでしょう.つまり,このコマンドバッファという仕組みそのものは少なくとも6年以上変わることなく続いている*2ことを意味します.
以下は『Accurately Profiling Direct3D API Calls』や『The CPU Aspect of the D3D Pipeline (PPT注意)』で触れられていなかった点について,いくつか予想など.

  • 複数のスレッドから 3D Device を利用した場合,コマンドバッファによって API 呼び出しがシリアライズされていると予想される.
  • 初期の DrawPrimitive や頂点バッファ導入以後の DrawXXPrimitiveUP は,描画用の頂点データをコマンドバッファに直接格納している.コマンドバッファが一杯になると強制的にカーネルモードの遷移が発生するが,DrawXXPrimitiveUP 系の使用が推奨されていない理由のひとつとして,コマンドバッファの消費速度が速い命令の使用は避けるべきという考えが予想される.
  • SetRenderState は描画命令発行直前に,1つのD3DDP2OP_RENDERSTATEとそれに続く D3DHAL_DP2RENDERSTATE 構造体の配列としてコマンドバッファに挿入される*3Improving Performance of Operation Handling によると,同一の Render State に対する変更が複数回あった場合,最後の呼び出しのみが記録されることが保証されているらしい.

次回は Longhorn の DDK を参照しつつ,WGF 世代のドライバモデルについて見ていこうと思います.

*1:初期の Direct3D での ExecuteBuffer を思い出す話です

*2:1997年8月4日にリリースされたDirectX 5.0 で DrawPrimitive が導入されました.また1999年9月22日にリリースされた DirectX 7.0 で始めて HW T&L がサポートされています.BBX の「DirectX の歴史」より

*3:『さいけでりっく☆さんどいっち : ステート変更のコスト』(id:eguo:20050815:p1)で言及されていたのはまさにこの点ですね