FullScreenでVSync同期している時のDirectX描画タイミング

Win7でFullScreenでVSync同期しているときの話です。

今回の描画プログラム

DX9のゲームのレンダリング。ただし、SwapChainのConfigは随時変更。OSはWindows7-x64でアプリもx64です。残念ながら描画スクリーンショットはお見せで来ません。描画に際し、100フレーム描画ごとに200ms程度のSleep()を挟んで、CPU,GPU共に完全にパイプラインがフラッシュされた状態からの描画の立ち上がりを観察します。

SwapChainDescは基本的に以下の通りで、BackbufferCountとWindowedを変化させて観察します。

static D3DPRESENT_PARAMETERS D3DPRESENT_PARAMETERS_0 = {
  1920, //BackBufferWidth
  1200, //BackBufferHeight
  D3DFMT_A8R8G8B8, //BackBufferFormat
  1, //BackBufferCount
  D3DMULTISAMPLE_NONE , //MultiSampleType
  0, //MultiSampleQuality
  D3DSWAPEFFECT_DISCARD, //SwapEffect
  g_hWnd, //hDeviceWindow
  1, //Windowed
  0, //EnableAutoDepthStencil
  D3DFMT_D24S8, //AutoDepthStencilFormat
  0, //Flags
  0, //FullScreen_RefreshRateInHz
  D3DPRESENT_INTERVAL_ONE  //FullScreen_PresentationInterval
};

先ずはWindowedの場合について

FullScreenモードに関して調べる前に、Windowedモードの場合の描画について、確認の意味も込めて少し調べます。

Windowed=1, BackbufferCount=1 の場合
Windowed_VSync_Present_BackBuffer1

Windowed=1, BackbufferCount=3 の場合
Windowed_VSync_Present_BackBuffer3

 赤い線は、Present()呼び出しの前後に挟んだEventのCPU時間を示します。GameApp.exeのスレッドが休止している区間が、Present()呼び出しによるブロックなので、その事よりPresent()の始まりと終わりが解ると思います。
 Windowedの場合は、Presnet()呼び出しがあると描画結果のBufferをWindowの領域にコピーする処理が行われます。VSync同期していないときは、このコピー処理は随時処理されますが、VSync同期している場合はVSyncを待ってからコピー処理が行われるようです。
 GPUViewのログを見る限りでは、BackbufferCountの数に関わらず、初回のPresent()呼び出しからブロックされているのが解ります。つまり、描画に使うBackbufferの数は実質1つしかなく、VSyncを待ってWindow領域にコピーされ、それが次のフレームで表示されることになっていると思われます。

FullScreenの場合について

Windowed=0, BackbufferCount=1 の場合
FullScreen_VSync_Present_BackBuffer1

Windowed=0, BackbufferCount=2 の場合
FullScreen_VSync_Present_BackBuffer2

Windowed=0, BackbufferCount=3 の場合
FullScreen_VSync_Present_Backbuffer3

 上記3つのLogを見ていただければ解るとおり、BackbufferCountの数によって挙動が変化します。青い線はVSyncのタイミングを表します。
 BackbufferCountが1の場合は、FrontBuffer, BackBufferと描画し、VSyncを跨いで表示バッファのFlipが発生し、再びBackBufferへの描画を行いPresent()の呼び出しを完了しますが、Present()の処理自体はスタックされ、次のVSyncのFlipが終わるまで処理されません。CPUはさらに次フレーム、次々フレームの描画を行っていますが、CPU側も3フレーム以上の先行動作はせず、その後はPresent()をブロックしてVSyncを待つ動作になっていきます。
 BackbufferCountが3の場合は、FrontBuffer, BackBuffer1, VSync, Backbuffer1, Backbuffer2, Vsync, Backbufeer2, Backbuffer3と描画して、Backbuffer3のPresent()がVSyncのブロックを受けています。以後はVSyncを跨ぐまで、常にPresent()がブロックされます。描画コマンドの先行は行われず、Present()呼び出しが完了したフレームで、GPU側もBackbufferへの描画を完了しています。BackbufferCountが2の場合は、丁度その中間になっているように見えます。

結局どれが

 CPU-GPU同期が全く行われないアプリの場合、ユーザーが観測するフレーム遅延の数は、FullScreenの場合は、SetMaximumFrameLatency()で設定されたフレーム数になると思われ、BackbufferCountによってBackbufferの数を変化させることで、描画コマンドの先行か、描画自体の先行かを選べるようになっているようです。Backbufferの数による使用GPUメモリの増加を除けば、BackbufferCountを3に設定した状態は、GPU側の描画が既に先行して終了しているため、CPUが他のアプリケーションなどによって、一時的に占有された場合でも、BackbufferのFlip動作さえ行うことが出来れば、安定した描画が可能になると思われます。
 一方で、BackbufferCountが1の場合は、描画コマンド自体はキューに積み上げられただけで、長時間未処理な状態となります。とはいえ、GPUへのDMA発行さえ出来れば、描画自体はGPUの仕事なので、これらの設定は好みの問題かもしれません。
 また、どのケースもいえることは、Present()の呼び出しによってブロックされる時間は、VSyncによるブロックに有無に関わらず長いということです。Prsent()のブロックには、VSyncの待ち時間もありますが、UMDが直前までのDrawCallを処理し、描画コマンドキューに積み上げる処理も含まれます。DrawCallボトルネックになっているアプリケーションなどでは、特にVSync同期をしていないにもかかわらずPresent()のブロック時間が長くなりがちです。描画のスレッドとゲームのスレッドを分けるか、もしくは最低でもPresent()の呼び出しは別のスレッドで行ったほうが良いかも知れません。
 ちなみにCPU-GPU同期が発生しているようなアプリケーションでは上記のような動作にはなりません。