タグ別アーカイブ: DirectX

DirectX関連の話題

D3D12のRoot Signature 1.1について

前提知識として、D3D12の基本的なResource Bindingが理解できているものとして進めます。
Windows 10 Anniversary Update(build 14393 または Version 1607)より、RootSignature1.1が導入されました。引き続きRootSignature1.0が使用可能ではありますが、RootSignature1.1に変更することによってどんなメリットがあるのか見ていきたいと思います。
続きを読む

フルスクリーン描画時のリフレッシュレート

DirectXで、フルスクリーン描画をする際のディスプレイのリフレッシュレートについてです。

最近のPC向けディスプレイ

近年のPC向けディスプレイは、さまざまな垂直同期周波数を持っているケースがあります。具体的には、60Hzのほかに、120Hzや、144Hzなどです。
これ等のリフレッシュレートは、垂直同期を有効にした場合でも、60Hzのディスプレイに比べ、高頻度に画面を書き換える機会を有しています。また、垂直同期を無効にした場合でも、同様に、高頻度に画面を書き換える機会を有しているため、ティアリングによる不快感を軽減(分断された絵の差分を最小限に)できる可能性があります。

ゲーム製作者側の留意点

ゲーム製作者側が留意しなくてはならないのは、当然ながら、垂直同期:有効=60Hzという前提でゲームを製作しないということです。これに関しては、殆どのPC向けのゲームでは、フレーム間の時間間隔は可変となっているので、問題になることは無いと思います。また、垂直同期を無効にしたケースではプログラム側は特に留意することは無いように思えます。
ただし一点だけ気をつけたほうが良いことがあります。それは、フルスクリーン描画を行う際の、RefreshRateの設定値です。デバイス作成時に設定するRefreshRateは、そのままディスプレイの垂直同期周波数として設定され、D3DPRESENT_INTERVALがどのように設定されても、その値が使用されます。
つまり、120Hzの垂直同期周波数をサポートするディスプレイでは、60Hz/120Hzいずれにも設定して、垂直同期を無効にしてフルスクリーン描画を行うことができます。その際に観測されるFPSは、基本的には全く差が無い状態となりますが、ディスプレイの駆動周波数は異なるため、プレイヤーの体験としては違うものになってしまいます。
では、どうすれば良いかですが、現在のディスプレイのRefreshRateを取得し、同一のRefreshRateで使用可能なスクリーン解像度を使用すると良いと思います。また、ゲーム内のメニューから明示的に設定できるようにしても良いと思います。

初期化の例

以下の例では、現在設定されているRefreshRateを尊重しつつ、19×10解像度のフルスクリーン描画の初期化を行うコードです。解像度が存在しない場合の処理などは省略されています。

if( NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )
  return E_FAIL;

D3DDISPLAYMODE curDM;
g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &curDM);

D3DDISPLAYMODE targetDM;
UINT targetRes[2] = {1920, 1080};

ZeroMemory(&targetDM, sizeof(targetDM));

// check if there is 19x10 resolution with current refresh rate and color depth.
UINT cnt = g_pD3D->GetAdapterModeCount(D3DADAPTER_DEFAULT,
      curDM.Format);
for (UINT i=0; iEnumAdapterModes(D3DADAPTER_DEFAULT,
  curDM.Format, i, &dm);

  if (curDM.Format == dm.Format &&
      curDM.RefreshRate == dm.RefreshRate &&
      targetRes[0] == dm.Width &&
      targetRes[1] == dm.Height) {
    targetDM = dm;
    break;
  }
}

// you'll need proper handler...
if (targetDM.Width != targetRes[0])
  return E_FAIL;

static D3DPRESENT_PARAMETERS d3dpp = {
  targetDM.Width, 
  targetDM.Height,
  targetDM.Format,
  2,
  D3DMULTISAMPLE_NONE,
  0,
  D3DSWAPEFFECT_DISCARD,
  hWnd,
  0, //Full screen
  0,
  D3DFMT_UNKNOWN,
  0,
  targetDM.RefreshRate, // set the refresh rate that have been set.
   //D3DPRESENT_INTERVAL_ONE // enable v-sync
  D3DPRESENT_INTERVAL_IMMEDIATE // disable v-sync
};

// create a device.
if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
 D3DCREATE_SOFTWARE_VERTEXPROCESSING,
 &d3dpp, &g_pd3dDevice ) ) )
 

DrawCallが9倍早くなるワケ

地表で生きるものとして、Windows上でのDrawCallの仕組みを見てみます。

DrawCallがCPU処理時間を消費する理由

DrawCallがプロセス側から発行されると、その時点で、描画に使用するリソース(VB,IB,Tex,CBなど)が、存在するかどうか、大きさは適切であるかなどの、妥当性のチェックが行われると同時に、リソースがGPU側のデバイスメモリに転送されているか、転送されていればそのアドレスはどこなのか、などの問題が解決され、GPU用の描画命令コマンドとして、専用のバッファに蓄積されます。
ここまでが、大まかですが、DrawCallの発行によって、CPU側で処理される内容です。
リソースの生成を伴わない、通常のDrawCallであれば、専用のバッファへの蓄積までは、UMD(User Mode Driver)で処理されます。UMDは、皆さんのプロセスと同じアドレス空間を用いて動作するもので、要はDLLのライブラリ等と同じです。KMDはこのケースでは動作することは殆どありません。(KMDについては後述)
では、何がCPU処理時間を消費しているのでしょうか。
上記で述べた、使用リソースの妥当性チェックと、リソースのオブジェクト名からアドレスへの変換と、API呼び出しのオーバーヘッドです。

ちなみに、DXでは、DXのAPIが呼び出されると、DXのレイヤーの処理に加え、必要に応じてそれに対応するUMDの関数が呼び出されますが、OGLの場合は、その処理のほとんどが、UMDに相当するレイヤーで行われます。従って、OGLの方が、全般的にAPIレイヤーのオーバーヘッドが少ないと思われます。
下記のリンクは、この違いが、実際の性能に現れたケースだと思われます。
[EXTREME TECH: Valve: OpenGL is faster than DirectX — even on Windows]
ただし、全てにおいてDXよりOGLの方が速いとは限りません。DXは多数のゲームタイトルで使用され、最適化も進んでいます。加えてOGLのように太古の昔からの互換性もありません。

KMDについて

私はWindowsのドライバの中を見たことも書いたことが無いので、以下は正確性を欠いているかもしれません。
KMDはKernel Mode Driverで、その名の通り、KernelModeで動作しているものです。KMDのインスタンスはシステム上で1つのみロードされており、GPUとの直接の通信を担当します。
対してUMDはプロセスごとにロードされるため、使用しているプロセスごとに、複数個存在する前提となっています。
KMDはGPUとの唯一の窓口であるため、GPU側のメモリの管理や、Apertureと呼ばれる、GPU側から見えるHOST側のメモリの管理を行っています。
KMDはUMDの要求に応じて、GPUのメモリやApertureの領域を割り当てます。UMDはそれを、自分のリソースとして管理しているため、GPU側の、自分の保持しているリソースのアドレスを知ることが出来ます。GPU側のメモリやAperture領域の確保は、UMD-KMD間のやり取りを必要とするため、非常にコストが高いです。
従って、小さなサイズのリソースに関しては、UMDがある程度の大きさでKMDよりメモリ空間を取得し、それをUMD側で分割して割り当てていることがあります。
つまり、グラフィックスAPI上で新規にリソースを作成したからといって、直ちにKMDでGPU側のメモリ確保を行っているとは限りません。
もうひとつKMDの大きな役割として、GPU処理命令のブロック(DMAパケット)のGPU側への送出があります。DMAパケットの生成の殆どはUMD側で行いますが、実際にGPUに送る作業はKMDが行います。KMDはGPUを使用するプロセスが複数存在する場合は、そのスケジューリングも行います。スケジューリングの粒度は、このDMAパケットになります。
ひとつのDMAパケットがどの程度のGPU処理時間を使用するかは分からないので、プロセス間でGPUの競合が起きた場合の切り替わりの速度は、このDMAパケットの処理時間に依存します。
このあたりの事情はWDDMが2.xになると大きく変わると思われますが、Windows8.1でもWDDMは1.3なので、今でも大枠でこの仕組みは変わっていないと思われます。
また、ディスプレイの解像度変更など、システム全体に影響のある処理はKMDが担当しています。

話を元に戻します

OGLでDrawCallのCPU処理時間について詳細に扱ったセッションが、GTC2013でありました。
[GTC2013 Advanced Scenegraph Rendering Pipeline]

このスライドの30ページの所に注目です。2400回のDrawCallのCPU側の処理時間を比較しています。
VBOと示されるのは、通常の頂点バッファを用いたDrawCallです。これを基準として考えます。
VABは頂点フォーマットの設定をオブジェクト化してBindできるようにしたものです。OGLでは通常は、頂点フォーマットの設定は多数のGL関数の呼び出しを伴いますが、これを用いると、頂点フォーマットの設定をオブジェクト化でき、一度のAPI呼び出しで設定できるようになります。DXで言うところの、FVFからCreateVertexDeclaration()への変更といったところでしょうか。これによって処理時間は、10%ほど短縮されています。
VBUMは、頂点バッファのアドレスを、プロセス側で先に取得しておき、DrawCallの発行時には、GPU上のアドレスを直接指定する方法です。これにより、DrawCallごとに発生していた、頂点バッファのオブジェクト名からGPU上のアドレスへの変換作業が省略することができ、50%以上CPU処理時間が短縮されています。この短縮された時間には、アドレス変換だけではなく、リソースの妥当性検証部分も含まれていると思われます。なぜならば、GPU側のアドレスを直接指定するため、UMDレベルでは、リソースの妥当性検証は不可能に近いので、行われていないと考えるのが自然でだからです。
BINDLESS INDIRECT HOSTは、CPU側で、頂点バッファと頂点フォーマットに相当するアドレス情報と、インデックスバッファのアドレスを指定した、DrawCallに相当する構造体の配列を作成し、これにより複数のDrawCallを一回のAPI呼び出しで処理するものです。これを用いることで90%近い処理時間の短縮が達成されています。
つまり、通常の手順でDrawCallを呼び出す代わりに、DrawCallの使用するリソースの指定にGPU側のアドレスを直接指定し、一時的なバッファに描画命令を蓄積して、DrawCallをある程度まとめて発行すれば、DrawCallの処理が9倍早くなるのも妥当だと思われます。
BINDLESS INDIRECT GPUは、DrawCallの内容を指定した構造体配列が、GPU側のメモリに格納されているケースです。こちらは、もはやCPUの処理時間を殆ど消費しません。
このように既存のAPIである、OpenGLでも、GPUベンダーによる拡張を使用することを前提にすると、Textureなどの他のリソースも殆どがGPU側のアドレスを取得でき、描画リソースを指定する際に使用できるようになっています。これ等を積極的に用いると、DrawCallのCPU側の処理時間を数倍オーダーで高速化することは十分可能です。