合成器时钟

概述

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

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

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

在此类型的循环中,假设有一个垂直空白 (vblank) 节奏。 目前还不清楚如果应用程序窗口跨两个扫描出相的监视器,或者完全具有不同频率的监视器,应用程序应执行哪些操作。 事实上,无论应用程序显示在哪个窗口上,DXGI 交换链 API 始终使用主监视器的节奏。 这会导致希望在所有监视器上顺利呈现的应用程序出现问题。 一个实际示例是辅助监视器上的视频播放,该监视器的刷新与主监视器不同;引入多个监视器后存在的方案;它不成比例地影响玩家,他们往往拥有用于辅助 UI 的 60Hz 监视器, (144+Hz) 游戏监视器的频率要高得多。

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

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

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

API

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

API 的每个部分都会影响或观察系统合成器的工作周期。 此工作周期是一个常规节奏,每个周期生成一个 Compositor Frame。 根据系统工作负荷、显示器数量和其他因素,该周期可能对齐,也可能不一致地显示 vblank。

等待合成器时钟

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

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

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

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

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

使用合成器时钟处理使用情况

应用程序当前通过 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);
}

提升合成器时钟

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

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

在 flip2 时提升的刷新率;墨迹书写以 flip8 结束,速率返回到 60Hz

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

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

如果请求 Boost 的应用程序终止,则来自应用的提升请求也将终止。 仍处于活动状态且具有多个提升请求的应用程序可以检查引用计数,以确定取消启动的次数。 提升调用是完全兼容的,即使系统不处于动态刷新速率模式,其中提升乘数为 1x。

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 上,也可能不工作。 这意味着,如果显示重复,或内容显示在多个位置,则应用程序、框架或遥测可以考虑所有这些内容。 但是,这些合成器帧将提供未组合帧的不完整信息,例如在交换链上的 iflip (独立翻转) 。

作为用法示例,基于合成交换链的新媒体基础基础结构依赖于 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。