Kompositoruhr

Überblick

Die Kompositoruhr-API bietet Statistiken und Framerate-Steuerung für die reibungslose Darstellung von Inhalten auf dem Bildschirm, im schnellstmöglichen Rhythmus und in einer Vielzahl von Hardwarekonfigurationen. Dies wird traditionell von DirectX-APIs behandelt. Diese haben jedoch starke Verbindungen zu festen Aktualisierungsraten und einzelnen Anzeigekonfigurationen. Hier sehen Sie beispielsweise einen vereinfachten Pseudocode, der zeigt, wie Apps in der Regel erstellt werden, um mit der Aktualisierungsrate der Anzeige zu zeichnen.

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

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

Bei dieser Art von Schleife wird davon ausgegangen, dass ein einzelner vertikaler Leerwert (vblank) vorhanden ist. Es ist nicht klar, was die Anwendung tun soll, wenn das Fenster zwei Monitore überspannt, deren Scanout außerhalb der Phase ist oder über unterschiedliche Frequenzen insgesamt verfügt. Tatsächlich verwendet die DXGI-Swapchain-API immer den Rhythmus des primären Monitors, unabhängig davon, auf welchem Fenster die Anwendung angezeigt wird. Dies verursacht Probleme für Anwendungen, die eine reibungslose Präsentation auf allen Monitoren wünschen. Ein beispiel für die Videowiedergabe auf einem sekundären Monitor mit einer anderen Aktualisierung als der primären; Ein Szenario, das seit der Einführung mehrerer Monitore vorhanden ist; und es wirkt sich unverhältnismäßig auf Gamer aus, die tendenziell einen 60Hz-Monitor für sekundäre UI haben, und einen viel höheren Frequenzmonitor (144+Hz) für Spiele.

Das zweite häufige Problem besteht darin, die Framerate basierend auf der Computerleistung anzupassen. Dies ist typisch für Videowiedergabeanwendungen, die wissen möchten, ob die Videoframes vom Benutzer zur erwarteten Zeit angezeigt werden oder ob Störungen die Präsentation ungleichmäßig machen, in der Hoffnung, die Präsentation für eine bessere Leistung anzupassen. Beispielsweise kann ein Videostreamingdienst zu einem Stream mit niedrigerer Qualität wechseln, wenn der Computer nicht in der Lage ist, die gewünschte Framerate in höchster Qualität aufrechtzuerhalten. Dies wird auch von DXGI-APIs behandelt und ist daher von der gleichen Architektur- und API-Expositionsbeschränkung betroffen.

Schließlich bietet die API Anwendungen die Möglichkeit, an einer neuen Framerate-Verstärkungsfunktion mit dem Namen Dynamic Refresh Rate teilzunehmen, wobei das System mit einer relativ niedrigen Aktualisierungsrate für normale Vorgänge ( z. B. 60Hz ) ausgeführt wird, aber es beschleunigt sich bis zu einer höheren Frequenz – z. B. 120Hz – wenn eine Anwendung bestimmte latenzempfindliche Vorgänge ausführt, z. B. Freihandeingaben mit einem Eingabestift, oder Das Verschieben per Fingereingabe. Das Boost-Feature ist vorhanden, da die Ausführung mit der hohen Frequenz 100% der Zeit aus sicht des Stromverbrauchs unertraglich ist. Aufgrund der gleichen Einschränkungen der DXGI-API ist das Wechseln der Aktualisierungsrate der Anzeige zu beliebigen Zeiten normalerweise teuer, wobei Benachrichtigungen über den Übertragungsmodus für alle Anwendungen geändert werden, und die Kosten aller Anwendungen, die Code ausführen, um auf die Änderung zu reagieren. Daher führt die Aktualisierungsratesverstärkung eine leichte Konfigurationsänderung durch, die keine Benachrichtigungen ausgibt, aber folglich von der meisten Anwendung abstrahiert werden muss, was weiterhin der Meinung ist, dass das System mit der niedrigeren Häufigkeit ausgeführt wird. Diese Virtualisierung funktioniert, indem Anwendungen nur alle anderen vblank- oder alle drei Vblanks oder ein anderes ganzzahliges Intervall ausgegeben werden, sodass die Anwendung eine effektive Aktualisierungsrate sieht, die eine ganze Zahl der tatsächlichen Häufigkeit ist. Auf diese Weise kann der vorhandene vblank-Mechanismus ohne zusätzliche Kosten verwendet werden, um eine perfekt normale niedrigere Frequenz zu generieren. Die ausgerichtete vblank wird durch einen dynamischen Aktualisierungsmodus im Betriebssystem (OS) dargestellt, z. B. 60Hz/120Hz. Beachten Sie, dass das Boost-Feature daher nur funktioniert, um bis zu einer höheren Frequenz zu erhöhen, nie zu einem niedrigeren, da es nicht gleich billig ist, künstliche Vblanks einzufügen, da es darum geht, echte Vblanks zu ignorieren.

Mit der Kompositoruhr-API kann Ihre Anwendung nicht nur anfordern, dass das System den Verstärkungsmodus eingibt oder verlässt, sondern auch die tatsächliche Aktualisierungsrate im betreffenden Modus beobachten, damit Sie Inhalte mit höherer Häufigkeit präsentieren können.

Die API

Es gibt drei Teile der API. Der erste bietet einen anzeigeunabhängigen Takt für Anwendungen, die bei mehreren Monitoren zur Bildfrequenz vorhanden sein sollen. Die zweite ermöglicht Es Anwendungen, eine Frequenzverstärkung mit dynamischer Aktualisierungsrate anzufordern. Der dritte bietet Statistiken zum Verhalten des Systemkompositionsmoduls, getrennt für jede einzelne Anzeige.

Jeder Teil der API beeinflusst oder beobachtet den Arbeitszyklus des Systemkompositors. Dieser Arbeitszyklus ist ein regelmäßiger Rhythmus, der einen Kompositorrahmen pro Zyklus erzeugt. Dieser Zyklus kann je nach Systemworkload, Anzahl der Displays und anderen Faktoren an vblanks ausgerichtet sein oder nicht ausgerichtet werden.

Warten Sie auf die Kompositoruhr

Dieses Signal soll die Verwendung der IDXGIOutput::WaitForVBlank Methode ersetzen und gleichzeitig eine höhere Flexibilität bei unterschiedlichen Aktualisierungsraten und die Vereinfachung von Nutzungsmustern für Entwickler bieten. Wie bei WaitForVBlankmuss das System wissen, ob eine Anwendung auf dieses Signal wartet oder nicht, sodass das System, wenn keine Anwendungen warten, die Grafikkarte leiten kann, um den vertikalen leeren Interrupt zu deaktivieren.

Dies ist für die Energieverwaltung von entscheidender Bedeutung, sodass die Architektur der API so eingeschränkt wird, dass es sich um einen Wartefunktionsaufruf handelt, anstatt ein Ereignis anzunehmen oder zurückzugeben (das Grafiksystem kann nicht bestimmen, ob es gewartet wird). Auf dieser niedrigen Ebene wird erwartet, dass Anwendungen diese API verwenden, um Renderingthreads zu steuern, die von allgemeinen UI-Threads getrennt sind, ähnlich wie IDXGIOutput::WaitForVBlank traditionell verwendet wird.

Wie in der Übersicht erwähnt, gibt es mehrere Aspekte, die Kompositoruhr für diese WaitForVBlank- nicht berücksichtigen kann.

  • Nächste vertikale Leere, wenn die Kompositoruhr nicht unbedingt aus der primären Anzeige stammt.
  • Aktivieren von Anwendungen mit variablen Raten auf Displays, die die dynamische Aktualisierungsrate unterstützen.
  • Aufwachen von Anwendungen für anwendungsdefinierte Ereignisse.

Im Allgemeinen wird erwartet, dass viele Anwendungen mit der Kompositoruhr synchronisiert bleiben sollen, um ihre Frames optimal zu zeitigen. Einige Ausnahmen können jedoch Medienframeworks und Spiele enthalten, die auf dem vertikalen Leerzeichen einer bestimmten Anzeige aktiviert werden müssen.

Behandeln der Verwendung mit Kompositoruhr

Anwendungen werden derzeit über den Mechanismus von DXGI bei jedem vertikalen Leeren aktiviert, weisen jedoch häufig andere Ereignisse auf, für die sie ebenfalls aktiviert werden müssen. Anstatt diese Ereignisse separat zu behandeln, kann die Kompositoruhr Handles für mehrere Ereignisse übernehmen und signalisieren im nächsten Frame und wann immer die Ereignisse ausgelöst werden. Die Anwendung kann dann von einem Signal aus reaktivieren und das Ereignis kennen, das dazu führte, dass es wach wurde.

Zyklus für Kompositoruhrereignisse

Die Kompositoruhr wird immer auf dem vertikalen Leerzeichen eines Monitors oder auf einem anderen Timer aktiviert. Wenn der Kompositor schläft, aber die Anzeige immer noch aktualisiert wird, wird dieses Signal weiterhin an der Blank der primären Anzeige ausgelöst.

C++-Beispiel

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);
}

Boost-Kompositoruhr

Wenn die Quelle der Kompositoruhr die dynamische Aktualisierungsrate unterstützt (dieses Feature ist in erweiterten Anzeigeeinstellungen aktiviert; nur für variable Aktualisierungsraten mit Supporttreibern verwendbar) kann das System dynamisch zwischen zwei Raten wechseln. Es gibt einen ungeboosten Modus, der in der Regel 60Hz ist, und eine erhöhte Rate, die in der Regel 2x höher bei 120Hz ist. Diese höhere Aktualisierungsrate sollte verwendet werden, um latenzrelevante Inhalte wie digitale Freihandeingaben zu verbessern. Das folgende Diagramm zeigt, wie das System zwischen der Ausführung mit einer Basis-60Hz-Rate (Flip 1) und dann für 6 Frames (2-7) mit digitaler Freihandzeit bei 120Hz wechselt. Wenn die digitale Freihandeingabe nicht mehr aktualisiert wird, wechselt das System wieder in einen 60Hz-Modus.

Hier ist eine Abbildung der dynamischen Framerate zur Verstärkung.

Aktualisierungsrate bei Flip2 erhöht; Freihanden endet durch Flip8, und die Rate kehrt zu 60Hz

Und hier erfahren Sie, wie DWM Boost-Anforderungen verarbeitet.

Flussdiagramm, das zeigt, wie DWM Boost-Anforderungen

Wenn eine Anwendung, die eine Boost anfordert, beendet wird, werden auch die Boostanforderungen der App beendet. Anwendungen, die noch mit mehreren Boostanforderungen aktiv sind, können die Referenzanzahl überprüfen, um zu bestimmen, wie oft die Freigabe aufheben soll. Das Erhöhen von Anrufen ist vollständig kompatibel, auch wenn das System nicht im Modus "Dynamische Aktualisierungsrate" ausgeführt wird, wobei der Verstärkungsmultiplikator 1x sein würde.

C++-Beispiel

In diesem Beispiel wird WM_TOUCH verarbeitet, um die Aktualisierungsrate zu erhöhen, wenn diese Anwendung Toucheingaben empfängt, mit der Absicht, eine reibungslosere, hochfrequente Touchverschiebung zu ermöglichen. Eine komplexere Anwendung kann die Gestikerkennung zuerst ausführen und nur dann höher erhöhen, wenn eine Verschiebung erkannt wird.

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;
}

Framestatistiken

Anmerkung

Wir erwarten, dass Anwendungen das Framestatistikfeature hauptsächlich für Telemetrie verwenden, nicht zum Anpassen von Inhalten.

Windows-Anwendungen übermitteln häufig Inhalte an den Kompositor, der an einer Vielzahl von Speicherorten über Anzeigeadapter und Bildschirme hinweg angezeigt wird. Wir werden nicht immer auf einem Bildschirm gerendert, weshalb wir in dieser API Zieleverwenden. Anstatt sich auf eine einzelne Statistik zu verlassen, die dargestellt wird, wenn ein Frame auf den Bildschirm trifft, DCompositionGetTargetStatistics bietet Framestatistiken für jeden Kompositorframe, während er auf jedes Ziel trifft. Der Kompositor arbeitet regelmäßig, was auf einer Vblank auftreten kann oder nicht. Dies bedeutet, dass, wenn eine Anzeige dupliziert oder an mehreren Stellen ein Inhalt angezeigt wird, dann kann die Anwendung, das Framework oder die Telemetrie alles berücksichtigen. Diese Kompositorframes stellen jedoch unvollständige Informationen zu Frames bereit, die nicht zusammengesetzt sind, z. B. in iflip (unabhängiges Kippen) in einer Swapchain.

Als Beispiel für die Verwendung basiert die neue Media Foundation-Infrastruktur auf der Grundlage der Kompositions-Swapchain sowohl auf DCompositionGetStatistics als auch auf DCompositionGetTargetStatistics zur Bestimmung der zusammengesetzten Präsentationsqualität über Telemetrie. Zusätzlich zu dieser API rufen sie eine separate API auf, wenn sich ihre Frames in iflip befinden und nicht zum Kompositor wechseln.

Für bestimmte Verwendungen erwarten wir, dass Anwendungen IDCompositionDevice::GetFrameStatistics verwenden, um eine Schätzung zu erhalten, wann der nächste Kompositorframe durch Überprüfen DCOMPOSITION_FRAME_STATISTICS::nextEstimatedFrameTimewird.

Zuerst fragt die Anwendung den letzten Frame ab, der sich auf den Status der Präsentation des Frames durch verschiedene Ausdrücke bezieht. Die Anwendung verfügt entweder über eine vorhandene frameId, die von der Kompositions-Swapchain bereitgestellt wird, oder zukünftige Schnittstellen, über die Informationen abgerufen werden sollen, oder sie kann DCompositionGetFrameId- aufrufen, um die aktuellste COMPOSITION_FRAME_ID der angegebenen COMPOSITION_FRAME_ID_TYPEabzurufen.

  • COMPOSITION_FRAME_ID_CREATED. Der Kompositor hat mit der Arbeit am Frame begonnen.
  • COMPOSITION_FRAME_ID_CONFIRMED. Die Frame-ID, in der CPU-Arbeit abgeschlossen ist und alle Präsentierten stattgefunden haben.
  • COMPOSITION_FRAME_ID_COMPLETED. DIE GPU-Arbeit wird für alle Ziele abgeschlossen, die einem Frame zugeordnet sind.

Anmerkung

COMPOSITION_Frame_ID nimmt monoton zu; daher können frühere Kompositorframes daraus abgeleitet werden.

Als Nächstes fragt die Anwendung grundlegende Informationen zum Kompositionsrahmen ab, und eine Liste der targetIds, die Teil des Frames sind, indem DCompositionGetStatisticsaufgerufen wird. Wenn für die Anwendung zielspezifische Informationen erforderlich sind, werden DCompositionGetTargetStatistics verwendet, um Informationen für die angegebene FrameId und targetId abzurufen.

C++-Beispiel

Das folgende Beispiel zeigt eine rollierende Sammlung von Framestatistiken aus der API, die dann in der TargetFrameRate-Funktion zusammengefasst werden, um zu ableiten, was die Framerate über einer Reihe von Frames war. Auch diese Art von Code wird in Telemetrie oder in Frameworks anstelle in einer Anwendung erwartet.

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;
        }
    }
};

Glossar

  • Target. Eine Bitmap, in der das Kompositionsmodul die visuelle Struktur rastert. Diese Bitmap ist in der Regel eine Anzeige.
  • Compositor frame. Ein Kompositorarbeitszyklus – dies ist nicht unbedingt eine Leere.