合成交换链编程指南

合成交换链 API 是 DXGI 交换链的精神继任者,它允许应用程序在屏幕上呈现和显示内容。 在 DXGI 交换链上使用此 API 有几个好处。 对应用程序提供了关于交换链状态的更精细控制,并且在如何使用交换链时提供了更多的自由度。 此外,API 为精确演示计时提供了更好的情景。

什么是演示?

演示是在屏幕上显示绘制操作结果的概念。 演示是演示的单个实例,即在屏幕上向单个缓冲区显示绘制操作结果的请求。 演示可以包含描述如何在屏幕上显示的其他属性。 在此 API 中,演示还可以有一个目标时间,这是一个系统相对时间戳(中断时间),用于描述演示应该显示的理想时间。 应用程序可以使用此功能更准确地控制屏幕上显示内容的速率,并将演示与系统中的其他事件(如音频轨道)同步。

演示的核心是同步。 也就是说,绘制操作通常由 GPU 执行,而不是由 CPU 执行,因此,它们在与最初发出操作的 CPU 的时间线异步的时间线上执行。 演示是提交给 GPU 的一种操作,可确保先前发布的绘制操作在缓冲区显示在屏幕上之前完成。

应用程序通常会随时间推移发出许多演示,并在发出演示时有多个纹理可供选择。 应用程序必须使用此 API 提供的同步机制,以确保一旦绘制并演示缓冲区,你就不会再次绘制到该缓冲区,直到该演示被显示并随后被后续演示中的新缓冲区替换。 否则,应用程序最初要演示的缓冲区内容可能会在屏幕上演示时被覆盖。

演示模式 — 合成、多平面覆盖和 iflip

系统可以通过几种不同的方式显示应用程序提供的缓冲区。

默认情况下,最简单的方法是将演示发送到 DWM,DWM 将根据演示的缓冲区呈现帧。 也就是说,在 DWM 发送到显示器的后缓冲区中存在表示缓冲区的副本(或者更准确地说,是 3D 渲染)。 显示演示的此方法称为合成。

显示演示的更高性能模式是直接将演示缓冲区扫描到硬件,并消除发生的副本。 显示演示的此方法称为直接扫描。 处理演示时,DWM 可以决定对硬件进行编程,以直接扫描出演示缓冲区,方法是将缓冲区分配给多平面覆盖平面(简称 MPO 平面),或直接将缓冲区翻转到硬件(称为直接翻转)。

显示演示的更高性能方法是让图形内核直接显示演示,并完全绕过 DWM。 这种演示方法称为独立翻转 (iflip)。 为了获得最佳性能,请使用 DXGI 翻转模式中同时介绍了多平面覆盖和 iflip。

合成是最容易支持的,但也是效率最低的。 表面需要经过特殊分配才能有资格进行直接扫描或 iflip,并且这种类型的特殊分配比合成交换链有更严格的系统要求。 它仅适用于 WDDM 3.0 及更高硬件。 因此,应用程序可以查询仅合成演示的 API 支持,还可以查询符合直接扫描或 iflip 条件的演示。

演示工厂、检查功能和演示管理器

应用程序将从合成交换链 API 中使用的第一个对象是演示工厂。 演示工厂由应用程序创建,并绑定到应用程序传递给要创建的调用的 Direct3D 设备,因此,具有与该设备关联的视频适配器的关联。

演示工厂公开用于检查当前系统和图形设备是否能够使用合成交换链 API 的方法。 可以使用 IPresentationFactory::IsPresentationSupported 等功能方法检查系统支持。 如果功能方法指示对 API 的系统支持,则可以使用演示工厂创建演示管理器。 此演示管理器是用于执行演示功能的对象,绑定到与用于创建演示的演示工厂相同的 Direct3D 设备和视频适配器。

目前,使用合成交换链 API 的系统要求是支持 WDDM(Windows 设备驱动程序模型)2.0 和 Windows 11(内部版本 10.0.22000.194)或更高版本的 GPU 驱动程序。 若要以性能最高的方式(直接扫描和独立翻转或 iflip)使用合成交换链 API,系统将需要支持 WDDM 3.0 的 GPU 驱动程序。

如果系统无法使用合成交换链 API,则应用程序需要有单独的代码路径来处理使用较旧的方法(如 DXGI 交换链)的演示。

注册要演示的演示缓冲区

演示管理器跟踪它可以演示的缓冲区。 为了演示 Direct3D 纹理,应用程序必须先使用 Direct3D 创建该纹理,然后将其注册到演示管理器。 当纹理注册到演示管理器时,它称为演示缓冲区,并且可以从该点开始由该演示管理器呈现到屏幕。 应用程序可以根据需要添加和删除演示缓冲区,但可以添加到单个演示管理器(当前为 31 个)的最大演示缓冲区数。 这些演示缓冲区也可以具有不同的大小和格式,这些大小和格式将在呈现单个演示缓冲区时生效。

纹理可以注册到任意数量的演示管理器;但是,在大多数情况下,这不会被视为正常使用,并且会提出应用程序负责管理的复杂同步方案。

定义要呈现的内容

通常,我们存在的缓冲区需要与可视化树中的内容相关联。 因此,我们需要定义一种绑定,这样当应用程序出现问题时,就可以清楚地看到所呈现的缓冲区在可视化树中的位置。 我们将这种绑定称为演示内容

呈现的内容可能采用多种形式。 应用程序可能希望显示单个缓冲区,或者可能希望向左眼和右眼提供缓冲区等立体声内容。 此 API 的初始版本支持将单个缓冲区呈现到屏幕。

我们将演示图面定义为一种呈现内容形式,其中一次显示单个缓冲区。 可以将演示图面设置为可视化树中的内容,并且可以一次在屏幕上显示单个演示缓冲区。 演示管理器演示将以原子方式更新一个或多个演示图面显示的缓冲区。

演示管理器可用于为给定合成图面句柄创建一个或多个演示图面。 每个合成图面句柄都可以绑定到可视化树中的一个或多个视觉对象(由 Windows.UI.CompositionDirectComposition 文档中概述的策略)来定义关联演示图面与其可视化树中显示的关系。 应用程序可以更新提交到系统的一个或多个演示图面,并在下一次演示操作中执行。

请注意,演示管理器可以将任何演示缓冲区呈现给它所需的任意数量的演示图面。 没有限制。 但是,由应用程序来跟踪已发布的缓冲区以及发布位置,以确保在演示图面仍在显示新图形时,不会尝试向该缓冲区发布新图形。

将属性应用于演示图面

除了指定要在演示图面中显示的缓冲区外,演示还可以指定该演示图面的各种其他属性。 这些属性包括定义如何采样源纹理的属性,包括 alpha 模式和颜色空间、如何转换和布局源纹理,以及受保护内容的任何显示或回退限制。 所有这些都作为属性资源库方法公开在演示图面上,应用程序可以更改这些方法,就像缓冲区更新一样,它们将在应用程序出现时生效。

呈现到演示图面

在应用程序创建演示图面、注册演示缓冲区并指定在演示期间发布的更新后,可以通过演示来应用这些属性。 应用程序通过演示管理器发出演示。 当系统处理该演示时,所有更新都会自动应用。 此外,应用程序还可以指定演示的其他属性,例如它应进行的理想时间(演示目标时间时间)和其他罕见的属性,例如预期内容速率,可用于在系统上启用自定义刷新模式。 由于演示可以在给定的时间安排,因此应用程序可以提前发出多个演示。 这些演示将在到达预定时间后逐一处理。

同步演示

应用程序必须确保,当它呈现到缓冲区并发出提示时,它会选择一个当前未被任何其他未完成的先前提示引用的缓冲区进行呈现,因为这样做可能会覆盖那些提示的缓冲区内容。 此外,如果应用程序向扫描硬件中的演示图面当前显示的缓冲区发出呈现问题,则其呈现可能会无限期停止,因为 Direct3D 不允许前台缓冲区呈现

合成交换链 API 提供了几种不同的机制,允许应用程序对它所提供的缓冲区进行适当的同步。

如果没有引用缓冲区的未完成呈现,并且系统当前未显示缓冲区,则称缓冲区可用。 否则,不可用。 API 为每个表示缓冲区提供一个事件,该事件指示缓冲区是否可用。 这是应用程序使用的最简单的同步方法。 在绘制到缓冲区并显示它之前,应用程序可以确保其可用事件得到信号通知。 特定缓冲区的可用事件在被绑定到 API 中的演示图面时变为无信号,并一直保持无信号状态,直到演示失效。

其次,演示管理器跟踪单个演示失效围栏,以与应用程序报告哪些演示已经完成。 围栏的值对应于开始其生命周期的停用阶段的最后一个演示的演示标识符,如下面的生命周期部分所述。 在演示进入此阶段后,可以放心地假定可以重复使用已替换为后续演示的任何缓冲区。

这种同步方法更先进,但允许对工作流的节流进行更大的控制,并且关于当前队列深度的系统状态的信息更丰富。 有关演示的生命周期的概述,请参阅以下部分。

演示的生命周期

演示管理器的演示作为其演示队列的一部分排队到系统。 系统按排队顺序处理演示。 此外,每个演示都有一个唯一的(对演示管理器)关联的演示标识符,这是分配给演示的递增值,从第一个演示的 1 开始,每个后续演示的递增值递增 1。 此演示标识符用于 API 的各个部分,例如同步基元和演示统计信息,以引用该特定演示。

应用程序发布的每个演示都遵循一个特定的生命周期,如本文所述。

一旦应用程序将更改设置为演示的一部分,它将使用演示管理器实际发布演示。 此时,演示被认为挂起

挂起后,演示将位于演示管理器的演示队列中,该队列将一直保留,直到发生以下两种情况之一。

  • 演示变为已取消。 演示管理器允许应用程序取消以前发布的演示。 如果发生这种情况,则演示被认为已取消,然后立即变为已停用。 在此转换中,将更新已取消演示的关联缓冲区可用 事件,但演示已停用的围栏不会发出信号,因为以前显示的演示(取消演示之前)将保持显示状态。 因此,应用程序无法使用演示停用围栏来确定哪些演示被取消。 相反,必须从每个已取消的演示状态统计信息中了解这一点。 建议应用程序使用缓冲区可用事件来查找一个可用的缓冲区,以便在取消后演示。 一旦显示了演示,上一个演示将开始停用过程,并更新演示停用围栏。
  • 如果不取消,则演示最终变为准备就绪可供处理。 要做好准备,必须满足两个主要条件。
    • 在调用演示之前,所有发布到 Direct3D 上下文的绘制工作都必须完成。 这样可以确保在应用程序的绘制完成之前不显示缓冲区。
    • 如果指定了演示目标时间,则我们希望能够显示演示系统相对时间满足应用程序应用于演示请求的目标时间。

当系统决定找到要在屏幕上显示的演示时,将选择最后一个成为准备就绪可供显示的演示。 如果有多个就绪演示,则除最近的(即具有最大当前标识符的演示)外的所有演示都将被跳过,并立即进入已停用状态,此时其缓冲区可用事件将被发信号通知,但演示停用围栏将不会被发信号,因为跳过的演示不会从显示状态转换。

当选择要显示的就绪演示时,系统开始工作以在屏幕上显示它。 这可能意味着将缓冲区呈现为 DWM帧 的一部分,然后请求硬件在屏幕上显示该帧;或者在 iflip 的情况下,这可能意味着将缓冲区直接发送给扫描硬件。 在此之后,演示被认为已排队。 在高级别上,这意味着它正在被显示。

当硬件开始显示演示时,该演示被认为已显示。 它会在屏幕上保持可见,直到随后的演示出现并替换它。

当后续演示排队时,我们就知道硬件最终会停止显示当前的演示。 此时,演示被认为即将停用

当随后的演示变为已显示时,则当前演示被认为已停用

演示管理器公开演示停用围栏,当它进入即将停用状态时,向每个演示的演示标识符发送信号。 此信号向应用程序表明,在不损坏以前的演示的情况下,向与该演示相关联的缓冲区发出演示工作已变得安全。 如果应用程序在演示处于即将停用状态期间发出呈现工作,则呈现工作将排队,直到演示进入已停用状态,此时将执行呈现工作。 如果呈现工作在演示变为已停用后发出,则它将立即执行。

以下是这种状态变化的示意图。

Lifecycle of a present

缓冲区、图面和演示的示意图

下面是与演示管理器、演示缓冲区、演示图面、演示和更新相关的关系图。

Diagram of buffers, surfaces, and presents

该图显示了一个演示管理器,具有两个演示图面和三个演示缓冲区,到目前为止,它已经发布了两个已发布的演示:第一个在图面 1 中显示缓冲区 1,第 2 个在图面 2 中显示缓冲区 2。 第二个演示更新了图面 2,以显示演示缓冲区 3,并且未更改图面 1 的绑定。 在显示演示 2 之后,图面 1 将显示缓冲区 1,图面 2 将显示缓冲区 3,这可以在演示管理器中对象的当前状态中看到。 队列中的每一个都将在系统中进行处理时生效。

注意

由于演示 2 未更改图面 1 的缓冲区,因此图面 1 与上一个演示的缓冲区 1 保持绑定。 从这个意义上说,在演示 2 中的缓冲区 1 上存在“隐式”引用,因为显示演示 2 后,图面 1 将保持绑定到缓冲区 1。

将演示图面添加到可视化树

演示图面是作为合成可视化树的一部分存在的内容。 每个演示图面都绑定到合成图面句柄。 在 Windows.UI.Composition 中,可以为预先存在的合成图面句柄创建图面画笔,并绑定到子画面视觉对象。 在 DirectComposition 中,可以从预先存在的合成图面句柄创建合成图面,并将其作为内容绑定到视觉对象。 有关详细信息,请参阅每个 API 的相应文档。

像 Windows Media Foundation 这样的 API 是为使用此 API 而构建的,它们公开将预先绑定到演示图面的合成图面句柄。 应用程序还可以创建自己的合成图面句柄,以便随后通过调用 DCompositionCreateSurfaceHandle 绑定到演示曲面并添加到可视化树。

读取演示统计信息

合成交换链 API 公开演示统计信息,其中描述了关于如何处理特定演示的各种信息。 这些信息通常描述在 DWM 帧中如何使用演示图面、显示的时间点、是否显示演示图面等。

有不同类型的演示统计信息,它们设计为可在 API 的未来版本中进行扩展。 应用程序使用演示管理器注册,接收感兴趣的统计信息类型。 然后,将这些统计信息推送到演示管理器的统计信息队列。 演示管理器向应用程序公开统计信息可用事件,这是一个事件句柄,指示统计信息队列何时具有可供读取的统计信息项。 当它出现时,应用程序可以将第一个统计信息项从队列中移出,读取并处理它。 当应用程序读取了队列中目前存在的所有统计信息时,演示管理器将重置统计信息可用事件。 应用程序通常会在循环中读取和处理统计信息,直到已重置统计信息可用事件。 应用程序通常会在用于发出演示的相同工作循环中处理此统计信息队列。 建议的使用模式是优先处理统计信息,而不是发布新的演示,以确保队列不会溢出。

队列将跟踪的统计信息的最大数量,大约是 512-1024 条统计信息。 在正常情况下,最大队列深度应足以存储约 5 秒的统计信息。 如果统计信息队列已满,并且报告了更多统计信息,则策略是,最旧的统计信息将停用以腾出空间。