Compartilhar via


Relógio do compositor

Visão geral

A API de relógio do compositor oferece estatísticas e controle de taxa de quadros para apresentar o conteúdo na tela de maneira suave, com a cadência mais rápida possível e em diversas configurações de hardware. Tradicionalmente, isso tem sido tratado pelas APIs do DirectX. Mas eles têm fortes laços com a taxa de atualização fixa e as configurações de exibição única. Por exemplo, aqui está uma parte simplificada do pseudocódigo mostrando como os aplicativos normalmente são criados para desenhar na taxa de atualização de exibição.

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

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

Nesse tipo de loop, a suposição é que há uma única cadência vertical em branco (vblank). Não está claro o que o aplicativo deve fazer se sua janela estiver atravessando dois monitores cuja verificação está fora de fase ou que têm frequências diferentes completamente. Na verdade, a API de troca DXGI sempre usa a cadência do monitor primário, independentemente da janela em que o aplicativo é exibido. Isso causa problemas para aplicativos que desejam apresentação suave em todos os monitores. Um exemplo real é a reprodução de vídeo em um monitor secundário que tem uma atualização diferente da primária; um cenário que existe desde que vários monitores foram introduzidos; e afeta desproporcionalmente os jogadores, que tendem a ter um monitor de 60Hz para interface do usuário secundária e um monitor de frequência muito maior (144+Hz) para jogos.

O segundo problema comum é o ajuste da taxa de quadros com base no desempenho do computador. Isso é típico para aplicativos de reprodução de vídeo, que desejam saber se os quadros de vídeo estão sendo vistos pelo usuário no momento esperado ou se as falhas estão tornando a apresentação desigual, na esperança de ajustar a apresentação para melhorar o desempenho. Por exemplo, um serviço de streaming de vídeo poderá alternar para um fluxo de baixa qualidade se o computador não for capaz de sustentar a taxa de quadros desejada na mais alta qualidade. Isso também é tratado por APIs DXGI e, portanto, é afetado pela mesma limitação de exposição de arquitetura e API.

Por fim, a API oferece aos aplicativos a oportunidade de participar de um novo recurso de aumento de taxa de quadros chamado Taxa de Atualização Dinâmica, pelo qual o sistema é executado a uma taxa de atualização relativamente baixa para operações normais, por exemplo, 60Hz, mas acelera até uma frequência mais alta, por exemplo, 120Hz, quando um aplicativo executa determinadas operações sensíveis à latência, como escrita à tinta com uma caneta, ou movimento panorâmico. O recurso de aumento existe porque a execução na alta frequência 100% do tempo é proibitiva de um ponto de vista de consumo de energia. Ao mesmo tempo, devido às mesmas limitações da API DXGI, alternar a taxa de atualização da exibição em horários arbitrários normalmente é caro, envolvendo notificações de alteração do modo de transmissão para todos os aplicativos e os custos de todos os aplicativos que executam o código para responder à alteração. Portanto, o recurso de aumento de taxa de atualização executa uma alteração de configuração leve que não emite nenhuma notificação, mas, consequentemente, deve ser abstraído da maioria dos aplicativos, que continuam acreditando que o sistema está em execução na frequência mais baixa. Essa virtualização funciona emitindo aplicativos apenas a cada vblank ou a cada três vblanks ou qualquer outro intervalo inteiro, para que o aplicativo veja uma taxa de atualização efetiva que é uma fração inteira da frequência real. Isso permite que o mecanismo vblank existente seja usado sem custo adicional para gerar uma frequência inferior perfeitamente regular. O vblank alinhado é representado por um modo de taxa de atualização dinâmica no sistema operacional (SO), como 60Hz/120Hz. Observe que, consequentemente, o recurso de aumento funciona apenas para aumentar para uma frequência mais alta, nunca para uma menor, pois não é igualmente barato inserir vblanks artificiais como é ignorar vblanks reais.

A API do relógio compositor permite que seu aplicativo não apenas solicite que o sistema entre ou saia do modo de aumento, mas também observe a taxa de atualização verdadeira quando estiver nesse modo, para que você possa apresentar conteúdo na frequência mais alta.

A API

Há três partes na API. A primeira oferece uma pulsação independente de exibição para aplicativos que desejam apresentar à taxa de quadros em vários monitores. A segunda permite que os aplicativos solicitem um aumento de frequência com a Taxa de Atualização Dinâmica. O terceiro oferece estatísticas sobre o comportamento do mecanismo de composição do sistema, separado para cada exibição individual.

Cada parte da API influencia ou observa o ciclo de trabalho do Compositor do Sistema. Esse ciclo de trabalho é uma cadência regular que produz um Quadro compositor por ciclo. Esse ciclo pode ou não estar alinhado para exibir vblanks, dependendo da carga de trabalho do sistema, do número de exibições e de outros fatores.

Aguarde o relógio compositor

A finalidade desse sinal é substituir o uso do método IDXGIOutput::WaitForVBlank , fornecendo maior flexibilidade em diferentes taxas de atualização e simplificando os padrões de uso para desenvolvedores. Assim como acontece com WaitForVBlank, o sistema precisa saber se um aplicativo está aguardando esse sinal ou não, para que, quando nenhum aplicativo estiver aguardando, o sistema possa direcionar o vídeo cartão para desativar a interrupção vertical em branco.

Isso é fundamental para o gerenciamento de energia, restringindo assim a arquitetura da API para ser uma chamada de função de espera, em vez de aceitar ou retornar um evento (o sistema gráfico não pode determinar se ele está ou não sendo aguardado). Nesse nível baixo, espera-se que os aplicativos usem essa API para controlar threads de renderização, separados dos threads de interface do usuário de uso geral, semelhante à maneira como IDXGIOutput::WaitForVBlank é usado tradicionalmente.

Conforme mencionado na visão geral, há vários aspectos que o relógio compositor pode acomodar para o qual WaitForVBlank não pode.

  • Próximo espaço em branco vertical quando o relógio compositor não é necessariamente originado da exibição primária.
  • Ativar aplicativos com taxas variáveis em exibições que dão suporte à Taxa de Atualização Dinâmica.
  • Ativar aplicativos para eventos definidos pelo aplicativo.

Em geral, espera-se que muitos aplicativos queiram permanecer sincronizados com o relógio compositor para melhor cronometrar seus quadros; mas algumas exceções podem incluir estruturas de mídia e jogos que precisam ser ativados no espaço em branco vertical de uma exibição específica.

Manipular o uso com o relógio compositor

Atualmente, os aplicativos são ativados em todos os espaços em branco verticais por meio do mecanismo do DXGI, mas geralmente têm outros eventos para os quais também precisam ser ativados. Em vez de manipular esses eventos separadamente, o relógio compositor pode usar alças para vários eventos e sinalizar no próximo quadro e sempre que os eventos forem acionados. Em seguida, o aplicativo pode ativar de um sinal, sabendo o evento que o fez despertar.

Ciclo para eventos de relógio compositor

O relógio compositor sempre será ativado no espaço em branco vertical de um monitor ou em outro temporizador. Quando o Compositor estiver dormindo, mas a tela ainda estiver sendo atualizada, esse sinal ainda será disparado no vblank da exibição Primária.

Exemplo 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 relógio compositor

Quando a origem do relógio compositor dá suporte à Taxa de Atualização Dinâmica (esse recurso é ativado em configurações de exibição avançadas; somente utilizável em exibições de taxa de atualização variável com drivers de suporte), o sistema poderá alternar dinamicamente entre duas taxas. Há um modo não ativado, que normalmente será de 60Hz e uma taxa aumentada que normalmente é 2x maior a 120Hz. Essa taxa de atualização mais alta deve ser usada para aprimorar o conteúdo sensível à latência, como escrita à tinta digital. O diagrama a seguir mostra como o sistema alterna entre a execução a uma taxa base de 60Hz (inverter 1) e, em seguida, para 6 quadros (2-7) com tinta digital cronometrada a 120Hz. Por fim, depois que a tinta digital não estiver mais sendo atualizada, o sistema alterna de volta para um modo de 60Hz.

Aqui está uma ilustração da taxa de quadros dinâmica para aumentar.

taxa de atualização aumentada no flip2; a escrita à tinta termina por flip8 e a taxa retorna para 60Hz

E veja como o DWM lida com solicitações de aumento.

fluxograma mostrando como o DWM lida com solicitações de aumento

Se um aplicativo que solicita um Boost for encerrado, as solicitações de aumento do aplicativo também serão encerradas. Os aplicativos que ainda estão ativos com várias solicitações de aumento podem marcar a contagem de referências para determinar quantas vezes o unboost. O aumento de chamadas é completamente compatível, mesmo que o sistema não esteja no modo de Taxa de Atualização Dinâmica, em que o multiplicador de aumento seria 1x.

Exemplo de C++

Este exemplo processa WM_TOUCH aumentar a taxa de atualização sempre que este aplicativo recebe entrada por toque, com a intenção de fornecer uma experiência de movimento panorâmico mais suave e de alta frequência. Um aplicativo mais sofisticado pode executar o reconhecimento de gestos primeiro e aumentar somente se uma panela for detectada.

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

Estatísticas de quadro

Observação

Esperamos que os aplicativos usem o recurso de estatísticas de quadro principalmente para telemetria, não para ajustar o conteúdo.

Os aplicativos do Windows geralmente enviam conteúdo para o compositor que é mostrado em uma variedade de locais entre adaptadores de vídeo e telas. Nem sempre renderizamos em uma tela, e é por isso que nesta API usamos destinos. Em vez de depender de uma única estatística para representar quando um quadro atinge a tela, DCompositionGetTargetStatistics oferece estatísticas de quadro para cada Quadro compositor à medida que atinge cada destino. O compositor funciona regularmente, o que pode ocorrer em um vblank ou não. Isso significa que, se uma exibição for duplicada ou um conteúdo for mostrado em vários locais, o aplicativo, a estrutura ou a telemetria poderão ser responsáveis por tudo isso. No entanto, esses quadros compositor fornecerão informações incompletas sobre quadros que não são compostos, como em iflip (inversão independente) em uma cadeia de troca.

Como exemplo de uso, a nova infraestrutura do Media Foundation baseada na cadeia de troca de composição depende de DCompositionGetStatistics e DCompositionGetTargetStatistics para fazer determinações de qualidade de apresentação composta por meio de telemetria. Além dessa API, eles chamarão uma API separada quando seus quadros estiverem em iflip e não forem para o compositor.

Para determinados usos, esperamos que os aplicativos usem IDCompositionDevice::GetFrameStatistics para receber uma estimativa de quando o próximo quadro compositor virá verificando DCOMPOSITION_FRAME_STATISTICS::nextEstimatedFrameTime.

Primeiro, o aplicativo consultará o último quadro relativo ao status da apresentação do quadro por meio de frases diferentes. O aplicativo terá uma frameId existente fornecida pela cadeia de troca de composição ou interfaces futuras sobre as quais deseja informações ou pode chamar DCompositionGetFrameId para recuperar o COMPOSITION_FRAME_ID mais recente do COMPOSITION_FRAME_ID_TYPE especificado.

  • COMPOSITION_FRAME_ID_CREATED. O Compositor começou a trabalhar no quadro.
  • COMPOSITION_FRAME_ID_CONFIRMED. A ID do Quadro na qual o trabalho de CPU foi concluído e todos os presentes ocorreram.
  • COMPOSITION_FRAME_ID_COMPLETED. O trabalho de GPU é concluído para todos os destinos associados a um quadro.

Observação

COMPOSITION_Frame_ID está aumentando monotonicamente; para que quadros compositores anteriores possam ser inferidos a partir dele.

Em seguida, o aplicativo consultará informações básicas sobre o quadro de composição e uma lista de targetIdsque fazem parte do quadro, chamando DCompositionGetStatistics. Por fim, se o aplicativo exigir informações por destino, ele usará DCompositionGetTargetStatistics para recuperar informações para o frameId e targetId especificados.

Exemplo de C++

O exemplo a seguir mostra uma coleção sem interrupção de estatísticas de quadro da API, que são resumidas na função TargetFrameRate para deduzir qual era a taxa de quadros em um conjunto de quadros. Novamente, esse tipo de código é esperado em telemetria ou em estruturas, em vez de em um aplicativo.

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

Glossário

  • Destino. Um bitmap no qual o mecanismo de composição rasteriza a árvore visual. Esse bitmap geralmente é uma exibição.
  • Quadro compositor. Um ciclo de trabalho compositor — isso não é necessariamente um vblank.