プログラムとかデジタル系趣味とか

WaitableTimerを使った固定FPS制御と、タイマー精度の話

各種タイマーによる固定フレームレート制御と、その精度のお話。

 

はじめに

固定FPSというとSleepかマルチメディアタイマーイベントの方法が出ると思います。

 


timeBeginPeriod(1);
auto elapsed = (timeGetTime() - startTime) - 16;
if(elapsed>0) {
    Sleep(elapsed);
}
startTime = timeGetTime();
timeEndPeriod(1);    



雑に書きましたが本当は何回か分でまとめて調整して精度上げたりそもそもQueryPerformanceCounterつかったりするでしょう。
今回はその方法自体についてとタイマー精度についてです。

WaitableTimerによる固定FPS

 WaitableTimerはタイマー機能つき同期オブジェクトです。

CreateWaitTimerでタイマーオブジェクトを作成し、SetWatableTimerでセット、WaitForSingleObject(など)で待ちを行います。
このタイマーは指定単位が100ns単位です。パフォーマンスカウンタなんて敵じゃありません。(その精度で動くとは言っていない)

インターバルコールバックなんかもできるようですが今回は使いません。
指定は相対と絶対がありますが今回は相対値で指定します。相対指定時はマイナス値を渡します。
Sleepと違ってタイマーなので、与える待ち時間は基本的には一定でいいはずです。

WaitForSingleObject;
SetWatableTimer(1フレーム時間);

擬似コードですが、基本的にはこんな感じでいいはずです。
ほぼ、それっぽく動きますが、問題があります。
タイマー精度とSetWatableTimer/WaitForSingleObject自体のコストです。

 

そこでこんな処理になります


void wait() {
	//timer object wait. one frame time each if to timeout.
	auto waitRet = WaitForSingleObject(timer_, (1000+fps_-1)/fps_);
	auto current = getTime();
	//タイマーがタイム・アウトしている場合はwait-wait間で
	//すでに時間が過ぎているものとして誤差調整処理の対象外にする
	auto sub = (current - preframeTime_) - freq_/fps_;
	auto delay = waitRet==WAIT_TIMEOUT;
	if (delay==false && preframeIsDelay_==false) {
		waitTime_ += sub;
	}
	preframeIsDelay_ = delay;
	SetWaitableTimer(timer_, (LARGE_INTEGER*)&waitTime_, 0, NULL, NULL, FALSE);
	preframeTime_ = getTime();

}

suffixアンダーバーはメンバ変数だと思ってください。 要は前回今回がタイマータイムアウトしていない場合正常と見なし、その間にかかった時間をタイマーとして必要な時間とします。
これで呼び出しコストの問題は解決します。

タイマー精度とNtSetTimerResolution

あとはタイマー精度の問題です。
これは環境依存で、最初から特に問題にならない精度の場合もありますが基本的には対処しておくべきです。
そして対処方法ですが、googleで検索なんかをすると大抵timeBeginPeriod~timeEndPeriodがでます。これで1ms精度が出せるわけですのでまあ優秀なんですが、
実はそれ以上を設定できる関数があるようです。

それが NtSetTimerResolution です。
しかしこれは公開されていません。ntdll.dllをLoadLibraryし、GetProcAddressで取得する必要があります。

LONG NtQueryTimerResolution(PULONG maximum, PULONG minimum, PULONG current);
まずはこの関数でその環境に設定可能な最大~最小タイマー精度と現在のタイマー精度がとれます。
LONG NtSetTimerResolution(PLONG resolution, BOOL set, PULONG current);
この関数で設定します。
第一引数に精度を、第二引数は0ならリセット、1でセットです。最後にはリセットしましょう。
第三引数からは現在値が返ってきます。

なお、これらの関数の単位は100nsです。
ですのでSetWaitableTimerと相性が良いですね。

テスト

f:id:misakichi-k:20181019005922p:plain

timeBeginPeriod(1)の結果がいまいち納得いきませんが、圧倒的にNtSetTimerResolutionでminを設定した場合が誤差が少ないのが見て取れると思います。

テストコードはgistに置いておきますのでお試しください。

下記テストコードはMIT Lisenceでお願いします。