コンポジター クロック

概要

コンポジター クロック API には、統計情報とフレーム レート制御機能があり、画面上のコンテンツをスムーズに、可能な限り高速に、さまざまなハードウェア構成で表示できます。 従来、これは DirectX API によって処理されてきました。 しかし、固定リフレッシュレートとシングルディスプレイ構成との強い関係があります。 たとえば、通常、アプリを作成して表示の更新速度で描画する方法を示す擬似コードの簡略化された部分を次に示します。

void GameLoop()
{
    CreateRenderingObjects();
    auto pSwapChain = CreateSwapChain();

    while (pSwapChain->WaitForVerticalBlank())
    {
        ProcessInput();
        RenderFrame(pSwapChain);
        pSwapChain->Present();
    }
}

この種類のループでは、1 つの垂直空白 (vblank) 周期が存在することを前提とします。 スキャンアウトが位相がずれている 2 台のモニター、または周波数が完全に異なる 2 台のモニターにまたがっている場合に、アプリケーションで何を行う必要があるかは明らかではありません。 実際、DXGI スワップチェーン API では、アプリケーションが表示されているウィンドウに関係なく、常にプライマリ モニターの周期が使用されます。 これにより、すべてのモニターでスムーズに表示するアプリケーションに問題が発生します。 実際の例の 1 つは、プライマリとは異なる更新を行うセカンダリ モニターでのビデオ再生です。複数のモニターが導入されて以来存在するシナリオ。これは、セカンダリ UI 用の 60Hz モニターと、ゲーム用のはるかに高い周波数 (144 + Hz) モニターを持つ傾向があるゲーマーに不釣り合いに影響します。

2 つ目の一般的な問題は、マシンのパフォーマンスに基づいてフレーム レートを調整することです。 これは、ビデオ再生アプリケーションで一般的です。これは、パフォーマンスを向上させるためにプレゼンテーションを調整するために、ビデオ フレームが予想される時間にユーザーに表示されているかどうか、または不具合によってプレゼンテーションが不均一になっているかどうかを知りたい場合です。 たとえば、マシンが目的のフレーム レートを最高の品質で維持できない場合、ビデオ ストリーミング サービスが低品質ストリームに切り替わる可能性があります。 これも DXGI API によって処理されるため、同じアーキテクチャと API の公開制限の影響を受けます。

最後に、API は動的リフレッシュ レートと呼ばれる新しいフレーム レート ブースト機能に参加する機会をアプリケーションに提供します。これにより、システムは通常の操作 (たとえば 60Hz) の比較的低い更新レートで実行されますが、アプリケーションがスタイラスによるインク処理など、特定の待機時間に依存する操作を実行すると、より高い頻度 (120Hz など) に高速化されます。 またはタッチ パン。 ブースト機能は、高周波数で100%の時間で実行することは電力消費の観点から非常に大きいために存在します。 同時に、DXGI API の制限が同じであるため、ディスプレイの更新レートを任意のタイミングで切り替えることは通常コストがかかります。すべてのアプリケーションに対するブロードキャスト モードの変更通知と、変更に応答するためにコードを実行しているすべてのアプリケーションのコストが関係します。 したがって、リフレッシュ レートブースト機能は、通知を発行しない軽量の構成変更を実行しますが、その結果、システムが低い頻度で実行されていると信じ続けるほとんどのアプリケーションから抽象化する必要があります。 この仮想化は、アプリケーションを他のすべての vblank、または 3 つの vblanks ごと、またはその他の整数間隔ごとに発行することによって機能し、アプリケーションが実際の頻度の整数分数である有効なリフレッシュ レートを確認できるようにします。 これにより、既存の vblank メカニズムを追加コストなしで使用して、完全に規則的な低い頻度を生成できます。 アラインされた vblank は、オペレーティング システム (OS) の動的リフレッシュ レート モード (60Hz/120Hz など) で表されます。 そのため、ブースト機能は、実際の vblanks を無視するため、人工 vblanks を挿入する場合と同様に安くはいかないので、より高い周波数までブーストするだけであることに注意してください。

コンポジター クロック API を使用すると、アプリケーションはシステムにブースト モードの開始または退出を要求するだけでなく、そのモードの場合に真のリフレッシュ レートを観察して、より高い頻度でコンテンツを表示できます。

API

API には 3 つの部分があります。 1 つ目は、複数のモニターでフレーム レートで表示するアプリケーションに対して、表示に依存しないハートビートを提供します。 2 つ目は、アプリケーションが動的リフレッシュ レートを使用して周波数ブーストを要求できるようにします。 3 つ目は、システム構成エンジンの動作に関する統計を、個々のディスプレイごとに区切って提供します。

API の各部分は、システム コンポジターの作業サイクルに影響を与えるか観察します。 この作業サイクルは、サイクルごとに 1 つのコンポジター フレームを生成する定期的な周期です。 システムのワークロード、ディスプレイの数、その他の要因に応じて、そのサイクルが vblanks を表示するように調整される場合とそうでない場合があります。

コンポジター クロックを待つ

このシグナルの目的は、 IDXGIOutput::WaitForVBlank メソッドの使用を置き換えながら、さまざまな更新レートの柔軟性を高め、開発者向けの使用パターンを簡略化することです。 WaitForVBlank と同様に、システムはアプリケーションがこの信号を待機しているかどうかを認識する必要があります。そのため、アプリケーションが待機していないときに、システムはビデオ カードに垂直の空白割り込みをオフにするように指示できます。

これは電源管理にとって重要であるため、イベントを受け入れたり返したりするのではなく、API のアーキテクチャを待機関数呼び出しに制限します (グラフィックス システムでは、待機中かどうかを判断できません)。 この低レベルでは、アプリケーションでは、 IDXGIOutput::WaitForVBlank の従来の使用方法と同様に、汎用 UI スレッドとは別に、この API を使用してレンダリング スレッドを制御することが期待されます。

概要で説明したように、 WaitForVBlank では対応できないコンポジター クロックに対応できるいくつかの側面があります。

  • コンポジタークロックが必ずしもプライマリディスプレイから供給されるとは限らない場合、次の垂直空白。
  • 動的リフレッシュ レートをサポートするディスプレイで、アプリケーションを可変レートでウェイクします。
  • アプリケーション定義イベントのアプリケーションのウェイクアップ。

一般に、多くのアプリケーションでは、フレームを最適なタイミングでコンポジター クロックと同期し続ける必要があります。ただし、一部の例外には、メディア フレームワークや、特定のディスプレイの垂直空白をスリープ解除する必要があるゲームが含まれる場合があります。

コンポジター クロックを使用した使用状況の処理

アプリケーションは現在、DXGI のメカニズムを通じてすべての垂直空白でスリープ解除されますが、多くの場合、他のイベントもウェイクする必要があります。 コンポジター クロックは、これらのイベントを個別に処理するのではなく、複数のイベントのハンドルを受け取り、次のフレームやイベントが発生するたびにシグナルを受け取ることができます。 その後、アプリケーションは 1 つのシグナルから復帰し、それが目覚める原因となったイベントを知ることができます。

コンポジター クロック イベントのサイクル

コンポジター クロックは、常にモニターの垂直空白、または別のタイマーでスリープ解除されます。 コンポジターがスリープ状態でもディスプレイが更新中の場合、このシグナルはプライマリ ディスプレイの vblank で引き続き発生します。

C++ の例

void GameLoop(HANDLE hQuitGameEvent)
{
    DWORD waitResult;

    CreateRenderingObjects();
    auto pSwapChain = CreateSwapChain();

    do
    {
        // Do all of the work for a single frame
        ProcessInput();
        RenderFrame(pSwapChain);
        pSwapChain->Present();

        // Wait for the compositor heartbeat before starting a new frame
        waitResult = DCompositionWaitForCompositorClock(1, &hQuitGameEvent, INFINITE);

        // If we get WAIT_RESULT_0+count it means the compositor clock ticked,
        // and we should render another frame. Our count is one, as we're
        // passing only one extra handle. Otherwise, either we got a failure or
        // another thread signaled our "quit" event, and in either case we want
        // to exit the loop
    } while (waitResult == WAIT_OBJECT_0 + 1);
}

ブースト コンポジター クロック

コンポジター クロックのソースが動的リフレッシュ レートをサポートしている場合 (この機能は高度なディスプレイ設定でオンになり、サポート ドライバーを使用した可変リフレッシュ レートディスプレイでのみ使用できます)、システムは 2 つのレートを動的に切り替えることができます。 通常は 60Hz で、ブースト レートは通常 120Hz で 2 倍高くなる、非ブースト モードがあります。 この高い更新レートを使用して、デジタル 手描き入力などの待機時間に依存するコンテンツを強化する必要があります。 次の図は、システムがベースの 60Hz レート (フリップ 1) で実行してから、デジタル インクが 120Hz でタイミングが設定された 6 フレーム (2 ~ 7) で実行を切り替える方法を示しています。 最後に、デジタル インクが更新されなくなったら、システムは 60Hz モードに戻ります。

ブースト用の動的フレームレートの図を次に示します。

flip2 でリフレッシュ レートがブーストされました。インクは flip8 で終了し、レートは 60Hz に戻ります

DWM がブースト要求を処理する方法を次に示します。

DWM がブースト要求を処理する方法を示すフローチャート

Boost を要求するアプリケーションが終了すると、アプリからのブースト要求も終了します。 複数のブースト要求でアクティブなアプリケーションは、参照カウントをチェックして、起動を解除する回数を決定できます。 ブースト呼び出しは、システムが動的リフレッシュ レート モードでない場合でも完全に互換性があり、ブースト乗算器は 1 倍になります。

C++ の例

このサンプルでは このアプリケーションがタッチ入力を受け取るたびに更新速度を向上させるためにWM_TOUCH処理します。これにより、より滑らかで高周波数のタッチ パン エクスペリエンスが提供されます。 より高度なアプリケーションでは、まずジェスチャ認識を実行し、パンが検出された場合にのみブーストを実行できます。

int g_activeTouchPoints = 0;

LRESULT OnTouch(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
    LRESULT result = 0;
    UINT inputCount = LOWORD(wParam);
    auto hTouchInput = reinterpret_cast<HTOUCHINPUT>(lParam);

    // Allocate room for touch data (assume throwing new)
    auto pInputs = new TOUCHINPUT[inputCount];

    if (GetTouchInputInfo(hTouchInput, inputCount, pInputs, sizeof(TOUCHINPUT)))
    {
        for (int index = 0; index < inputCount; index++)
        {
            auto& touchInput = pInputs[index];

            // The first time we receive a touch down, boost the compositor
            // clock so we do our stuff at high frequency. Once the last touch
            // up happens, return to the base frequency
            if (touchInput.dwFlags & TOUCHEVENTF_DOWN)
            {
                if (!g_activeTouchPoints)
                {
                    // We're going from zero to one active points -- boost 
                    DCompositionBoostCompositorClock(true);
                }

                g_activeTouchPoints++;
            }
            else if (touchInput.dwFlags && TOUCHEVENTWF_UP)
            {
                g_activeTouchPoints--;

                if (g_activeTouchPoints == 0)
                {
                    DCompositionBoostCompositorClock(false);
                }
            }

            // Perform other normal touch processing here...
        }

        // We handled the window message; close the handle
        CloseTouchInputHandle(hTouchInput);
    }
    else
    {
        // We couldn't handle the message; forward it to the system
        result = DefWindowProc(hWnd, WM_TOUCH, wParam, lParam);
    }

    delete[] pInputs;
    return result;
}

フレームの統計情報

注意

アプリケーションでは、コンテンツの調整ではなく、主にテレメトリにフレーム統計機能を使用することが想定されています。

Windows アプリケーションは、多くの場合、ディスプレイ アダプターと画面全体のさまざまな場所に表示されるコンポジターにコンテンツを送信します。 常に画面にレンダリングされるとは限りません。そのため、この API ではターゲットを使用 しますDCompositionGetTargetStatistics は、フレームが画面にヒットしたときに表す 1 つの統計に依存するのではなく、すべてのターゲットにヒットするすべてのコンポジター フレームのフレーム統計を提供します。 コンポジターは定期的に動作します。これは vblank で発生する可能性があります。そうでない場合もあります。 つまり、ディスプレイが重複している場合、またはコンテンツが複数の場所に表示されている場合は、アプリケーション、フレームワーク、またはテレメトリがそのすべてに対応できます。 ただし、これらのコンポジター フレームは、スワップチェーンの iflip (独立したフリップ) など、構成されていないフレームに関する不完全な情報を提供します。

使用法の例として、コンポジション スワップチェーンに基づく新しい Media Foundation インフラストラクチャは 、DCompositionGetStatisticsDCompositionGetTargetStatistics の両方に依存して、テレメトリを通じて構成されたプレゼンテーション品質を決定します。 この API に加えて、フレームが iflip にあり、コンポジターに移動しない場合は、別の API を呼び出します。

特定の用途では、アプリケーションで IDCompositionDevice::GetFrameStatistics を使用すると、 DCOMPOSITION_FRAME_STATISTICS::nextEstimatedFrameTime をチェックすることで、次のコンポジター フレームがいつ来るのかの見積もりを受け取ることを想定しています。

最初に、アプリケーションは、フレームのプレゼンテーションの状態に関連する最後のフレームに対して、さまざまなフレーズを使用してクエリを実行します。 アプリケーションには、コンポジション スワップチェーンによって提供される既存の frameId、または情報が必要な今後のインターフェイスが存在するか、DCompositionGetFrameId を呼び出して、指定したCOMPOSITION_FRAME_ID_TYPEの最新のCOMPOSITION_FRAME_IDを取得できます。

  • COMPOSITION_FRAME_ID_CREATED。 コンポジターがフレームの作業を開始しました。
  • COMPOSITION_FRAME_ID_CONFIRMED。 CPU の作業が完了し、プレゼントが行われたフレーム ID。
  • COMPOSITION_FRAME_ID_COMPLETED。 フレームに関連付けられているすべてのターゲットに対して GPU の作業が完了します。

注意

COMPOSITION_Frame_ID は単調に増加しています。そのため、以前のコンポジター フレームを推論できます。

次に、アプリケーションは DCompositionGetStatistics を呼び出して、コンポジション フレームに関する基本情報と、フレームの一部である targetIdの一覧を照会します。 最後に、アプリケーションでターゲットごとの情報が必要な場合は、 DCompositionGetTargetStatistics を使用して、指定された frameId と targetId の情報を取得します。

C++ の例

次の例は、API からのフレーム統計のローリング コレクションを示しています。その後、 TargetFrameRate 関数に要約して、フレームレートが一連のフレームに対して何であったかを推測します。 ここでも、この種のコードは、アプリケーションではなく、テレメトリまたはフレームワークで想定されます。

class FrameStatisticsCollector
{
private:
    // Collect at most 4 target monitors
    static constexpr UINT sc_maxTargetCount = 4;

    struct CompositionTargetStats
    {
        COMPOSITION_FRAME_ID frameId;
        COMPOSITION_FRAME_STATS frameStats;

        COMPOSITION_TARGET_ID targetId;
        COMPOSITION_TARGET_STATS targetStats;
    };

    UINT64 m_qpcFrequency;
    COMPOSITION_FRAME_ID m_lastCollectedFrameId = 0;
    std::vector<CompositionTargetStats> m_targetStats;

public:
    FrameStatisticsCollector()
    {
        QueryPerformanceFrequency(&m_qpcFrequency);
        m_lastCollectedFrameId = CurrentFrameId();
    }

    // Queries the compositor clock statistics API to determine the last frame
    // completed by the composition engine
    COMPOSITION_FRAME_ID CurrentFrameId() const
    {
        COMPOSITION_FRAME_ID frameId;
        if (FAILED(_DCompositionGetFrameId(frameIdType, &frameId)))
        {
            frameId = 0;
        }

        return frameId;
    }

    // Queries the system to get information about the latest composition frames
    void CollectStats()
    {
        COMPOSITION_FRAME_ID currentFrameId = CurrentFrameId(COMPOSITION_FRAME_ID_COMPLETED);

        while (m_active && (currentFrameId > m_endFrameId))
        {
            auto newEndFrameId = m_endFrameId + 1;

            COMPOSITION_FRAME_STATS frameStats = {};
            COMPOSITION_TARGET_ID targetIds[sc_maxTargetCount] = {};
            UINT targetCount;

            hr = _DCompositionGetStatistics(newEndFrameId,
                &frameStats,
                _countof(targetIds),
                targetIds,
                &targetCount);
            if (SUCCEEDED(hr))
            {
                // We track up to sc_maxTargetCount targets per frame
                targetCount = min<UINT>(targetCount, _countof(targetIds));

                for (UINT uIndex = 0; uIndex < targetCount; uIndex++)
                {
                    COMPOSITION_TARGET_STATS targetStats = {};
                    hr = DCompositionGetTargetStatistics(newEndFrameId,
                        &targetIds[uIndex],
                        &targetStats);
                    if (SUCCEEDED(hr))
                    {
                        CompositionTargetStats compTargetStats = { newEndFrameId,
                                                                  frameStats,
                                                                  targetIds[uIndex],
                                                                  targetStats };

                        m_compTargetStats.push_back(compTargetStats);
                    }
                    else
                    {
                        m_active = false;
                    }
                }

                m_endFrameId = newEndFrameId;
            }
            else
            {
                m_active = false;
            }
        }
    }

    // Compute the frame rate for the given composition target in frames per
    // second, over the specified frame interval based on historical statistics
    // data
    float TargetFrameRate(
        _const COMPOSITION_TARGET_ID& targetId,
        COMPOSITION_FRAME_ID beginFrameId,
        COMPOSITION_FRAME_ID endFrameId)  const
    {
        UINT frameCount = 0;
        UINT64 beginTime = 0;
        UINT64 endTime = 0;

        for (const auto& stats : m_compTargetStats)
        {
            if ((stats.frameId >= beginFrameId) && (stats.frameId <= endFrameId))
            {
                if (stats.frameId == beginFrameId)
                {
                    beginTime = stats.frameStats.startTime;
                }

                if (stats.frameId == endFrameId)
                {
                    endTime = stats.frameStats.startTime +
                        stats.frameStats.framePeriod;
                }

                if ((stats.targetId == targetId) &&
                    (stats.targetStats.presentTime != 0))
                {
                    frameCount++;
                }
            }
        }

        if ((beginTime != 0) &&
            (endTime != 0) &&
            (endTime > beginTime) &&
            (frameCount != 0))
        {
            auto seconds = static_cast<float>(endTime - beginTime) /
                static_cast<float>(m_qpcFrequency);

            return static_cast<float>(frameCount) / seconds;
        }
        else
        {
            return 0.0f;
        }
    }
};

用語集

  • [ターゲット] で、 コンポジション エンジンがビジュアル ツリーをラスター化するビットマップ。 通常、このビットマップはディスプレイです。
  • コンポジター フレーム。 1 つのコンポジター 作業サイクル —これは必ずしも vblank ではありません。