Compositor 时钟

概述

合成器时钟 API 提供统计信息和帧速率控制,用于以最快的节奏和各种硬件配置流畅地呈现屏幕内容。 传统上,这由 DirectX API 处理。 但这些与固定刷新率和单一显示配置有着很强的联系。 例如,下面是一段简化的伪代码,其中显示了应用通常如何创作以显示刷新率绘制。

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

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

在此类型的循环中,假设有一个垂直空白(vblank)节奏。 如果应用程序窗口跨出两个监视器,其扫描已失阶段或完全具有不同频率的监视器,则不清楚应用程序应该执行什么作。 事实上,DXGI 交换链 API 始终使用主监视器的节奏,而不考虑应用程序显示的窗口。 这会导致希望在所有监视器上顺利呈现的应用程序出现问题。 一个实际示例是辅助监视器上的视频播放,该监视器的刷新与主要监视器不同;由于引入了多个监视器,存在一种方案;它不成比例地影响玩家,他们倾向于拥有 60Hz 的辅助 UI 监视器,以及游戏的更高频率(144+Hz)监视器。

第二个常见问题是,根据计算机性能调整帧速率。 这是典型的视频播放应用程序,他们希望知道视频帧是否在预期时间被用户看到,或者故障是否使演示文稿不均衡,希望调整演示文稿以提高性能。 例如,如果计算机无法以最高质量维持所需的帧速率,则视频流服务可能会切换到低质量流。 这也由 DXGI API 处理,因此受同一体系结构和 API 公开限制的影响。

最后,API 为应用程序提供了参与名为动态刷新速率的新帧速率提升功能的机会,其中系统以相对较低的刷新速率运行正常作(例如 60Hz),但当应用程序执行某些延迟敏感作(例如,使用触笔墨迹书写)时,它会加快到更高的频率(例如 120Hz) 或触摸平移。 提升功能存在,因为以 100% 的高速时间运行从电源消耗的角度来看是令人望而却步的。 同时,由于 DXGI API 的相同限制,在任意时间切换显示刷新率通常很昂贵,涉及向所有应用程序发送广播模式更改通知,以及运行代码以响应更改的所有应用程序的成本。 因此,刷新速率提升功能执行轻量配置更改,该更改不会发出任何通知,但因此,必须从大多数应用程序抽象出来,后者仍认为系统以较低的频率运行。 此虚拟化的工作原理是仅每隔一个 vblank、每三个 vblank 或任何其他整数间隔颁发应用程序,以便应用程序看到一个有效的刷新率,即实际频率的整数分数。 这允许使用现有的 vblank 机制,无需额外付费即可生成完全定期的较低频率。 对齐的 vblank 由作系统 (OS) 中的动态刷新速率模式表示,例如 60Hz/120Hz。 请注意,因此,提升功能仅适用于提升到更高的频率,从不降低到较低的频率,因为插入人工 vblank 并不同样便宜,因为它忽略真正的 vblank。

合成器时钟 API 不仅允许应用程序请求系统进入或离开提升模式,还可以在该模式下观察真正的刷新率,以便你可以以更高的频率显示内容。

The API

API 有三个部分。 第一个为希望在多个监视器上以帧速率显示的应用程序提供与显示器无关的检测信号。 第二个允许应用程序使用动态刷新速率请求频率提升。 第三个提供有关系统组合引擎行为的统计信息,这些引擎针对每个单独的显示进行分隔。

API 的每个部分都会影响或观察 System Compositor 的工作周期。 此工作周期是一个常规节奏,每个周期生成一个 Compositor Frame。 该周期可能与显示 vblank 保持一致,具体取决于系统工作负荷、显示器数量和其他因素。

等待合成器时钟

此信号的目的是替换 IDXGIOutput::WaitForVBlank 方法的使用,同时在不同的刷新速率中提供更高的灵活性,并简化开发人员的使用模式。 与 WaitForVBlank一样,系统需要知道应用程序是否正在等待此信号,以便当没有应用程序等待时,系统可以指示视频卡关闭垂直空白中断。

这对于电源管理至关重要,因此将 API 的体系结构限制为等待函数调用,而不是接受或返回事件(图形系统无法确定它是否正在等待)。 在此低级别,应用程序应使用此 API 来控制呈现线程,与常规用途 UI 线程分开,类似于传统上使用 IDXGIOutput::WaitForVBlank 的方式。

如概述中所述,组合器时钟可以适应该 WaitForVBlank 无法适应的几个方面。

  • 当合成器时钟不一定源自主显示器时,下一个垂直空白。
  • 在支持动态刷新速率的显示器上以可变速率唤醒应用程序。
  • 唤醒应用程序定义的事件。

一般情况下,预计许多应用程序希望与合成器时钟保持同步,以充分利用其帧时间:但某些例外情况可能包括媒体框架,以及需要在特定显示器的垂直空白上唤醒的游戏。

使用 compositor 时钟处理使用情况

应用程序当前通过 DXGI 的机制在每个垂直空白处唤醒,但通常还有其他事件需要唤醒。 合成器时钟可以处理多个事件,并在下一帧和事件触发时发出信号,而不是单独处理这些事件。 然后,应用程序可以从一个信号唤醒,知道导致它唤醒的事件。

合成器时钟事件的周期

合成器时钟将始终在监视器的垂直空白处或另一个计时器上唤醒。 当 Compositor 处于睡眠状态,但显示器仍在更新时,主显示器的 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);
}

Boost compositor 时钟

当合成器时钟的源支持动态刷新速率(该功能在高级显示设置中处于打开状态;只有可变刷新率显示支持驱动程序时可用)系统才能在两种速率之间动态切换。 有一个未启动的模式,它通常为 60Hz,而提升速率通常为 2 倍,在 120Hz。 应使用此更高的刷新率来增强延迟敏感内容,例如数字墨迹书写。 下图显示了系统如何在以 60Hz 基速率(翻转 1)和 6 帧(2-7)与数字墨迹计时为 120Hz 的数字墨迹之间切换。 最后,一旦数字墨迹不再更新,系统就会切换回 60Hz 模式。

下面是用于提升的动态帧速率的插图。

翻转时提升的刷新速率;墨迹书写以 flip8 结尾,速率返回 60Hz

以下是 DWM 处理提升请求的方式。

显示 DWM 如何处理提升请求的流程图 流程图

如果请求提升的应用程序已终止,则来自应用的提升请求也将终止。 仍使用多个提升请求处于活动状态的应用程序可以检查引用计数,以确定取消启动的次数。 提升调用完全兼容,即使系统不在动态刷新速率模式下,提升乘数将是 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 为每个 Compositor Frame 提供帧统计信息,而不是依赖单个统计信息来表示何时到达屏幕。 合成器会定期工作,该工作可能发生在 vblank 上,也可能不是。 这意味着,如果显示重复,或者内容显示在多个位置,则应用程序、框架或遥测可以考虑所有这些内容。 但是,这些合成器帧将提供未撰写的帧不完整的信息,例如,在交换链上 (独立翻转)时

作为用法示例,基于组合交换链的新媒体基础基础结构依赖于 DCompositionGetStatisticsDCompositionGetTargetStatistics,通过遥测确定组合演示文稿质量。 除了此 API,当帧位于 iflip 且不会进入合成器时,它们将调用单独的 API。

对于某些用法,我们希望应用程序使用 IDCompositionDevice::GetFrameStatistics 通过检查 DCOMPOSITION_FRAME_STATISTICS::nextEstimatedFrameTime来估计下一个合成器帧何时到来。

首先,应用程序将通过不同的短语查询与帧呈现状态相关的最后一帧。 应用程序将具有由组合交换链提供的现有 frameId,或者希望获取相关信息的未来接口,也可以调用 DCompositionGetFrameId 来检索指定 COMPOSITION_FRAME_ID_TYPE的最新 COMPOSITION_FRAME_ID

  • COMPOSITION_FRAME_ID_CREATED。 Compositor 已开始处理帧。
  • 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;
        }
    }
};

词汇表

  • 目标。 合成引擎将可视化树光栅化到其中的位图。 此位图通常是一个显示。
  • Compositor 帧。 一个 Compositor 工作周期 - 这不一定是 vblank。