Reloj del compositor

Información general

La API de reloj de compositor ofrece estadísticas y control de velocidad de fotogramas para presentar contenido en pantalla sin problemas, con la cadencia más rápida posible y en una variedad de configuraciones de hardware. Tradicionalmente, esto se ha controlado por las API de DirectX. Pero tienen vínculos fuertes con la frecuencia de actualización fija y las configuraciones de visualización única. Por ejemplo, este es un fragmento simplificado de pseudocódigo que muestra cómo las aplicaciones se crean normalmente para dibujar a la velocidad de actualización de la pantalla.

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

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

En este tipo de bucle, la suposición es que hay una sola cadencia en blanco vertical (vblank). No está claro lo que debe hacer la aplicación si su ventana está estrabando dos monitores cuyo examen está fuera de fase o que tienen diferentes frecuencias por completo. De hecho, la API de cadena de intercambio DXGI siempre usa la cadencia del monitor principal, independientemente de la ventana en la que se muestre la aplicación. Esto provoca problemas para las aplicaciones que quieran una presentación fluida en todos los monitores. Un ejemplo real es la reproducción de vídeo en un monitor secundario que tiene una actualización diferente a la principal; un escenario que existía desde que se introdujeron varios monitores; y afecta desproporcionadamente a los jugadores, que tienden a tener un monitor de 60Hz para la interfaz de usuario secundaria, y un monitor de frecuencia mucho mayor (144+Hz) para juegos.

El segundo problema común es el de ajustar la velocidad de fotogramas en función del rendimiento de la máquina. Esto es típico para las aplicaciones de reproducción de vídeo, que quieren saber si el usuario ve los fotogramas de vídeo en el momento esperado, o si los problemas están haciendo que la presentación sea desigual, con la esperanza de ajustar la presentación para mejorar el rendimiento. Por ejemplo, un servicio de streaming de vídeo podría cambiar a una secuencia de menor calidad si la máquina no es capaz de mantener la velocidad de fotogramas deseada a la máxima calidad. Esto también se controla mediante las API de DXGI y, por tanto, se ve afectado por la misma limitación de exposición de api y arquitectura.

Por último, la API ofrece a las aplicaciones la oportunidad de participar en una nueva característica de aumento de velocidad de fotogramas denominada Frecuencia de actualización dinámica, en la que el sistema se ejecuta a una velocidad de actualización relativamente baja para las operaciones normales (por ejemplo, 60Hz), pero acelera hasta una frecuencia superior (por ejemplo, 120Hz), cuando una aplicación realiza determinadas operaciones sensibles a la latencia, como la entrada manuscrita con un lápiz óptico, o movimiento panorámico táctil. La característica de aumento existe porque la ejecución con una frecuencia alta del 100 % del tiempo es prohibitiva desde un punto de vista del consumo de energía. Al mismo tiempo, debido a las mismas limitaciones de la API DXGI, cambiar la frecuencia de actualización de la pantalla en momentos arbitrarios suele ser costoso, implicando notificaciones de cambio de modo de difusión a todas las aplicaciones y los costos de todas esas aplicaciones que ejecutan código para responder al cambio. Por lo tanto, la característica de aumento de velocidad de actualización realiza un cambio de configuración ligero que no emite ninguna notificación, pero, por lo tanto, debe abstraerse de la mayoría de las aplicaciones, lo que sigue creyendo que el sistema se está ejecutando con la menor frecuencia. Esta virtualización funciona mediante la emisión de aplicaciones solo todas las demás vblank, o cada tres vblanks, o cualquier otro intervalo entero, para que la aplicación vea una frecuencia de actualización efectiva que sea una fracción entera de la frecuencia real. Esto permite que el mecanismo vblank existente se utilice sin costo adicional para generar una frecuencia inferior perfectamente normal. La vblank alineada se representa mediante un modo de frecuencia de actualización dinámica en el sistema operativo (SO), como 60Hz/120Hz. Tenga en cuenta que, por lo tanto, la característica de aumento solo funciona para aumentar hasta una frecuencia más alta, nunca a una inferior, ya que no es igualmente barato insertar vblanks artificiales, ya que es omitir vblanks reales.

La API de reloj compositor permite que la aplicación no solo solicite que el sistema entre o deje el modo de aumento, sino también para observar la velocidad de actualización verdadera cuando está en ese modo, para que pueda presentar contenido con mayor frecuencia.

La API

Hay tres partes en la API. La primera ofrece un latido independiente de la pantalla para las aplicaciones que quieren presentar a velocidad de fotogramas en varios monitores. La segunda permite a las aplicaciones solicitar un aumento de frecuencia con la frecuencia de actualización dinámica. La tercera ofrece estadísticas sobre el comportamiento del motor de composición del sistema, separados para cada pantalla individual.

Cada parte de la API influye o observa el ciclo de trabajo del compositor del sistema. Este ciclo de trabajo es una cadencia regular que genera un marco compositor por ciclo. Ese ciclo puede o no estar alineado para mostrar vblanks, en función de la carga de trabajo del sistema, el número de pantallas y otros factores.

Espere el reloj del compositor.

El propósito de esta señal es reemplazar el uso del método IDXGIOutput::WaitForVBlank , a la vez que proporciona mayor flexibilidad en diferentes velocidades de actualización y simplifica los patrones de uso para los desarrolladores. Al igual que con WaitForVBlank, el sistema debe saber si una aplicación está esperando esta señal o no, para que cuando ninguna aplicación esté esperando el sistema pueda dirigir la tarjeta de vídeo para desactivar la interrupción vertical en blanco.

Esto es fundamental para la administración de energía, lo que restringe la arquitectura de la API para que sea una llamada de función de espera, en lugar de aceptar o devolver un evento (el sistema de gráficos no puede determinar si se espera o no). En este nivel bajo, se espera que las aplicaciones usen esta API para controlar los subprocesos de representación, independientes de los subprocesos de interfaz de usuario de uso general, de forma similar a cómo se usa IDXGIOutput::WaitForVBlank .

Como se mencionó en la información general, hay varios aspectos que el reloj compositor puede dar cabida a que WaitForVBlank no puede.

  • A continuación, en blanco vertical cuando el reloj del compositor no tiene necesariamente origen en la pantalla principal.
  • La reactivación de aplicaciones a velocidades variables en pantallas que admiten la frecuencia de actualización dinámica.
  • Despertando aplicaciones para eventos definidos por la aplicación.

En general, se espera que muchas aplicaciones quieran mantenerse sincronizadas con el reloj compositor para mejorar el tiempo de sus fotogramas; pero algunas excepciones pueden incluir marcos multimedia y juegos que necesitan reactivarse en el espacio vertical en blanco de una pantalla específica.

Controlar el uso con el reloj de compositor

Las aplicaciones se activan actualmente en cada blanco vertical a través del mecanismo de DXGI, pero a menudo tienen otros eventos para los que también necesitan reactivarse. En lugar de controlar estos eventos por separado, el reloj del compositor puede tomar identificadores para varios eventos y indicar en el siguiente fotograma y cada vez que se activan los eventos. Después, la aplicación puede reactivarse desde una señal, sabiendo el evento que lo hizo despertar.

Ciclo para eventos de reloj de compositor

El reloj del compositor siempre se reactivará en el blanco vertical de un monitor o en otro temporizador. Cuando el compositor está dormido, pero la pantalla todavía se está actualizando, esta señal se activará en la vblank de la pantalla principal.

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

Aumentar el reloj del compositor

Cuando el origen del reloj del compositor admite la frecuencia de actualización dinámica (esa característica está activada en la configuración de pantalla avanzada; solo se puede usar en pantallas de frecuencia de actualización variable con controladores de soporte técnico), el sistema podrá cambiar dinámicamente entre dos velocidades. Hay un modo no activado, que normalmente será de 60Hz y una velocidad potenciada que normalmente es 2 veces mayor a 120Hz. Esta velocidad de actualización más alta debe usarse para mejorar el contenido sensible a la latencia, como la entrada manuscrita digital. En el diagrama siguiente se muestra cómo cambia el sistema entre la ejecución a una velocidad base de 60Hz (voltear 1) y, a continuación, para 6 fotogramas (2-7) con tinta digital con tiempo de 120Hz. Por último, una vez que la entrada de lápiz digital ya no se actualiza, el sistema vuelve a un modo de 60Hz.

Esta es una ilustración de la velocidad de fotogramas dinámica para aumentar.

frecuencia de actualización incrementada en flip2; la entrada manuscrita termina por flip8 y la velocidad vuelve a 60Hz

Y aquí se muestra cómo DWM controla las solicitudes de aumento.

diagrama de flujo que muestra cómo DWM controla las solicitudes de aumento

Si se finaliza una aplicación que solicita un aumento, también se finalizarán las solicitudes de aumento de la aplicación. Las aplicaciones que siguen activas con varias solicitudes de aumento pueden comprobar el recuento de referencias para determinar cuántas veces se va a deshacer. Las llamadas de aumento son totalmente compatibles, incluso si el sistema no está en modo de frecuencia de actualización dinámica, donde el multiplicador de aumento sería 1x.

Ejemplo de C++

Este ejemplo procesa WM_TOUCH para aumentar la frecuencia de actualización cada vez que esta aplicación recibe entrada táctil, con la intención de proporcionar una experiencia de movimiento panorámico táctil más suave y de alta frecuencia. Una aplicación más sofisticada podría realizar primero el reconocimiento de gestos y aumentar solo si se detecta un movimiento panorámico.

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

Estadísticas de fotogramas

Nota:

Esperamos que las aplicaciones usen la característica de estadísticas de fotogramas principalmente para la telemetría, no para ajustar el contenido.

Las aplicaciones de Windows suelen enviar contenido al compositor que se muestra en una variedad de ubicaciones en pantallas y adaptadores de pantalla. No siempre se representa en una pantalla, por lo que en esta API usamos destinos. En lugar de depender de una sola estadística que represente cuando un fotograma alcanza la pantalla, DCompositionGetTargetStatistics ofrece estadísticas de fotogramas para cada marco compositor a medida que alcanza cada destino. El compositor trabaja periódicamente, lo que puede producirse en una vblank o puede no hacerlo. Esto significa que si una pantalla está duplicada o un contenido se muestra en varios lugares, la aplicación, el marco o la telemetría pueden tener en cuenta todo. Sin embargo, estos marcos de compositor proporcionarán información incompleta sobre los fotogramas que no están compuestos, como en iflip (volteo independiente) en una cadena de intercambio.

Como ejemplo de uso, la nueva infraestructura de Media Foundation basada en la cadena de intercambio de composición se basa en DCompositionGetStatistics y DCompositionGetTargetStatistics para realizar determinaciones de calidad de presentación compuesta mediante telemetría. Además de esta API, llamarán a una API independiente cuando sus fotogramas estén en iflip y no vayan al compositor.

Para determinados usos, esperamos que las aplicaciones usen IDCompositionDevice::GetFrameStatistics para recibir una estimación de cuándo vendrá el siguiente marco de compositor comprobando DCOMPOSITION_FRAME_STATISTICS::nextEstimatedFrameTime.

En primer lugar, la aplicación consultará el último fotograma relacionado con el estado de la presentación del marco a través de frases diferentes. La aplicación tendrá un frameId existente proporcionado por la cadena de intercambio de composición o interfaces futuras sobre las que quiere información, o bien puede llamar a DCompositionGetFrameId para recuperar el COMPOSITION_FRAME_ID más reciente del COMPOSITION_FRAME_ID_TYPE especificado.

  • COMPOSITION_FRAME_ID_CREATED. El Compositor ha empezado a trabajar en el marco.
  • COMPOSITION_FRAME_ID_CONFIRMED. Identificador de fotograma en el que se completa el trabajo de CPU y se han realizado los regalos.
  • COMPOSITION_FRAME_ID_COMPLETED. El trabajo de GPU se completa para todos los destinos asociados a un marco.

Nota:

COMPOSITION_Frame_ID está aumentando monotónicamente; por lo que los marcos de compositor anteriores se pueden deducir de él.

A continuación, la aplicación consultará información básica sobre el marco de composición y una lista de targetIds que forman parte del marco mediante una llamada a DCompositionGetStatistics. Por último, si la aplicación requiere información por destino, usará DCompositionGetTargetStatistics para recuperar información del frameId y targetId especificados.

Ejemplo de C++

En el ejemplo siguiente se muestra una colección gradual de estadísticas de fotogramas de la API, que luego se resumen en la función TargetFrameRate para deducir cuál era la velocidad de fotogramas en un conjunto de fotogramas. De nuevo, este tipo de código se espera en telemetría o en marcos, en lugar de en una aplicación.

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

Glosario

  • Destino. Mapa de bits en el que el motor de composición rasteriza el árbol visual. Este mapa de bits suele ser una pantalla.
  • Marco compositor. Un ciclo de trabajo compositor: esto no es necesariamente un vblank.