フレームレートを制御する

Direct3Dに限った話ではありませんが、PCのアプリは、フレームレートを制御する必要があります。 ゲーム機のように、固定されたリフレッシュレートに合わせてフレームレートを決めることができる場合は、 VSyncを待つだけでフレームレートを制御できますが、PCのモニターはリフレッシュレートが様々なので、アプリ自身がフレームレートを制御する必要がでてきます。

一番簡単な方法は、前フレームからの時間を計測しておき、その時間が指定したフレームレートより早い場合に、Sleepさせる方法だと思います。

こんな感じです。

class FrameController {
public:
    FrameController() : mFPS(60.0f), mSleepCounter(0) {
        QueryPerformanceFrequency(&mFrequency);
        QueryPerformanceCounter(&mPrevious);
        mStart = mPrevious;
    }
    virtual ~FrameController() {}

    double GetTimeFromStart() const
    {
        LARGE_INTEGER now;
        QueryPerformanceCounter(&now);
        double duration_sec = static_cast<double>(now.QuadPart - mStart.QuadPart) / mFrequency.QuadPart;
        return duration_sec;
    }

    float Update(float fps = 0.0f)
    {
        // Sleepの精度を変更
        timeBeginPeriod(1);
        mSleepCounter = 0;

        // 前フレームからの時間を計測
        LARGE_INTEGER now;
        QueryPerformanceCounter(&now);
        double duration_sec = static_cast<double>(now.QuadPart - mPrevious.QuadPart) / mFrequency.QuadPart;
        if (fps > 0.0f)
        {
            // Sleepさせてフレーム間隔を制御
            double limit_sec = 1.0 / fps;
            while (duration_sec < limit_sec)
            {
                // 残り時間の半分までSleepさせる
                const double SLEEP_RATIO = 0.5;
                int sleep_msec = static_cast<int>(1000.0 * (limit_sec - duration_sec) * SLEEP_RATIO);
                Sleep(sleep_msec);
                ++mSleepCounter;
                // 時間を再計測
                QueryPerformanceCounter(&now);
                duration_sec = static_cast<double>(now.QuadPart - mPrevious.QuadPart) / mFrequency.QuadPart;
            }
        }
        // Sleepの精度を戻す
        timeEndPeriod(1);

        // フレームレートを更新(平均とるのは面倒なのでWEIGHTを掛ける)
        float current_fps = float(1.0 / duration_sec);
        float FPS_WEIGHT = 0.1f;
        mFPS = mFPS * (1.0f - FPS_WEIGHT) + current_fps * FPS_WEIGHT;

        // 前フレームの時間を更新
        mPrevious = now;

        return mFPS;
    }

    // デバッグ用
    int GetSleepCounter() const
    {
        return mSleepCounter;
    }

private:
    LARGE_INTEGER mFrequency;
    LARGE_INTEGER mPrevious;
    LARGE_INTEGER mStart;

    float mFPS;

    // デバッグ用
    int mSleepCounter;
};

使い方は、フレームの先頭で、以下のようにUpdateを呼びます。

float frame_rate = frameController.Update(100.0f);

試してみたところ、Sleepの精度が低く、1msよりも細かい単位ではSleepさせることはできませんでした。また、デフォルトでは、1msでも無理でしたので、 以下のように精度を1ms単位と指定しました。

        // Sleepの精度を変更
        timeBeginPeriod(1);

        // Sleep
        Sleep(sleep_msec);

        // Sleepの精度を戻す
        timeEndPeriod(1);

なお、このコードは、Sleepを徐々に短い時間で眠らせて、正確な時間のSleepを実現したかったのですが、実際は、数千回以上の Sleep(0) のビジーループで 正確な時間が経つのを待っています。もっと良いやり方もあるかもしれませんが、とりあえす、こんな感じで。