Oculus Rift DK2のDirectHMDモードにおけるTimeWarpの実装について

DK2をDirectHMDモードで接続すると、TimeWarpと呼ばれる、体感遅延を最小限に留める仕組みを使うことが出来るようになるようです。この仕組みを見てみました。

DistortionRenderの処理時間計測

OVRのSDKでは、両眼のレンダリングイメージから、レンズのDistortionをキャンセルしたイメージをBackBufferにレンダリングする部分と、SwapBufferを行う処理は、
DistortionRenderer::EndFrame(bool swapBuffers)
に実装されています。
特に、Backbufferに対するレンダリングは、
DistortionRenderer::renderEndFrame()
に実装されており、このなかで、DistortionRenderのDrawCallの呼び出しと、DK2のLatencyTester用のレンダリング(センサー付近に小さなQuadを描画)が行われています。
まずは、これらの描画処理にかかる時間を計測するために、

WaitUnitlGpuIdle();
renderEndFrame();
WaitUnitlGpuIdle();

という流れで処理します。
WaitUnitlGpuIdle()は、DXのEventQueryを発行し、取得できるまでブロックする関数です。つまり、処理の前後でGPU-CPUの同期を取り、その区間をCPUタイマーで計測します。計測では10数フレーム分の計測結果の平均値を採用しています。

TimeWarpPointTimeの計算

DistortionRenderの処理時間が計測できたら、次は、TimeWarpPointTimeを計算します。TimeWarpPointTimeは、DistortionRender->SwapBufferの一連処理を行い、かつ、VSyncに間に合うタイミングの時間位置を指します。具体的には、
NextFrameTime – (distortionRenderTime+3.5ms)
でOVRSDKでは計算されています。つまり、計測された処理時間に3.5msの余裕を持たせた時間になります。

TimeWarpの実行

TimeWarpPointTimeの計算が終わると、DistortionRenderの処理時間計測は行われなくなり、TimeWarpが有効に動作するようになります。するとDistortionRender以後の処理は、TimeWarpPointTimeまで処理が実行されず、Waitする処理へと切り替わります。
具体的には、下記のようになり、DistortionRenderより前の描画コマンドをFlushして、TimewarpPointTimeまで待ち、DistortionRenderを行う処理になります。このようにすることで、出来る限り最新のIMUとPositionalTrackingの情報を元に、描画したシーンがスクリーンに表示される時間における視点の姿勢の予測を行うようにしているようです。

FlushGpuAndWaitTillTime(TimeManager.GetFrameTiming().TimewarpPointTime);
renderEndFrame();

DistortionRender内での、warpMatricesの取得と計算

シーンの描画開始時と、TimewarpPointTime時の姿勢予測位置の差分を表すwarpMatricesは片眼あたり二つのmatrixを用います。

ovrMatrix4f timeWarpMatrices[2];
ovrHmd_GetEyeTimewarpMatrices(HMD, (ovrEyeType)eyeNum,
                              RState.EyeRenderPoses[eyeNum], timeWarpMatrices);

取得されるのは、次Frameの発光開始時間(次Frameの開始時間+LatencyTesterで計測された遅延時間)と終了時間(発光開始時間+Frame時間)における、姿勢マトリクスの予測値と、シーンのレンダリングに使用したマトリクスとの差分マトリクスです。この二つのMatrixをDistortionRendererのShaderにUniformとして引き渡しています。
 Shader内では、頂点のAlpha値(Scanlineの方向に基づくフレーム内の発光時間が各頂点に0~1.0で格納されているはず)を用いて、上記の2つのマトリクスをlerpで補間して、最終的な姿勢補正に用いています。ちなみにPixelShaderでの、DistortionRenderのサンプリング数は、Chromaの補正しなければ1Sample/pix、補正を行えば3sample/pixで、高品位にリサンプリングを行うようにすると、15Sample/pixとなります。

GPUViewで観測すると

OVR_timewarp
こんな感じ。

感想

 まず、Timewarpの仕組みに相当する部分は、OVRSDKにソース込みで実装されているので、読めば、さらに詳しいことが簡単に分かりますので、興味のある方は読んだほうが早いです。
 Timewarpが有効になるには、LatencyTesterが有効になっている必要があり、そのためには、DirectHMD接続になっている必要があるようです。また、ひとたびTimewarpが有効になれば、最低でもTimewarpPointTimeで、CPUとGPUが同期する必要があるので、OVRのレンダリングでよく言われるFrame遅延の類を気にする必要は無いと思います。
 また、このタイミングまでにGPUにおける両眼のレンダリングが完了し、DXのEventQueryが返ってくる必要があります。従って、描画コマンドの投入はこれよりもさらに前のタイミングで終わっている必要があります。加えて、DistortionRenderが終わった後は、SwapBufferを呼び出す以外に処理を進める方法はなく、OVR使用時のSwapBufferは、次フレの0.5ms付近まで処理をブロックする模様です。(図の赤い線はPresent()呼び出しの前後を表します)私の手元では、DistortionRenderは高品位版(15 sample/pix)にした場合、1.0ms程度処理にかかりましたので、SDKが設定するTimeWarpPointTimeはVSyncの4.5ms前となり、そこまでに描画処理が完了しなければならないので、GPUを使える時間は、
13.3 – (0.5+4.5) = 8.3ms
と、なかなか厳しい値となります。
加えて、この8.3msは正味全てを使うことが出来ません。なぜならば、SwapBufferから戻ってきた時点では、描画コマンドは全くキューに詰まれていないので、GPUは少なからずアイドルすることになります。
 改善点として挙げられるのは、まず、GPUViewのチャートを見る限り、DistortionRenderの完了後に数msの空き時間があるので、私の機材に限った話をするならば、TimewarpPointTimeはもう少し後ろにずらせると思います。このタイミングを後ろにずらせれば、その分シーンのレンダリングに使えるGPU時間が増えるはずです。現在のSDKが行っている、余裕をもったTimewarpPointの計算では、今後フレームレートが高くなると、さらにGPUが使用できる時間が短くなることが予測されるので、今後はこの部分の改善が必要になると思われます。