本文介绍 GPU 围栏同步对象,该对象可用于 GPU 硬件计划阶段 2 中真正的 GPU 到 GPU 同步。 从 Windows 11 版本 24H2 (WDDM 3.2) 开始支持此功能。 图形驱动程序开发人员应熟悉 WDDM 2.0 和 GPU 硬件计划阶段 1。
WDDM 2.x 的受监视围栏同步对象
WDDM 2.x 的受监视的围栏同步对象支持以下操作:
1. CPU 在受监视的围栏值上等待,方法是:
使用 CPU 虚拟地址 (VA) 轮询。
在 Dxgkrnl 内排队等待阻止,当 CPU 观察到新的受监视围栏值时,会发出信号。
2. 受监视值的 CPU 信号。
3. 通过写入受监视围栏 GPU VA,并引发受监视围栏信号中断,通知 CPU 值更新,从而获得受监视值的 GPU 信号。
不支持的是本机 ON-GPU 等待受监视的围栏值。 相反,OS 保留的 GPU 工作取决于 CPU 上的等待值。 仅当发出值信号时,它才会将此工作发布到 GPU。
添加了 GPU 本机围栏同步对象
从 WDDM 3.2 开始,监视的围栏对象已扩展,以支持以下新增功能:
- GPU 等待受监视的围栏值,这允许高性能引擎到引擎同步,而无需 CPU 往返。
- 有条件的中断通知只针对有 CPU 等待程序的 GPU 栅栏信号。 此功能允许 CPU 在所有 GPU 工作排队时进入低功率状态,从而节省大量电源。
- GPU 本地内存中的围栏值存储(而不是系统内存)。
GPU 本机围栏同步对象设计
下图演示了 GPU 本机围栏对象的基本体系结构,重点介绍 CPU 和 GPU 之间共享的同步对象状态。
该示意图包括两个主要组成部分:
-
当前值(本文中称为 CurrentValue)。 此内存位置包含当前用信号发送的 64 位围栏值。 CurrentValue 被映射到 CPU 并可访问 CPU(可从内核模式写入、可从用户和内核模式读取)和 GPU(使用 GPU 虚拟地址可读和可写)。 CurrentValue 需要 64 位写入从 CPU 和 GPU 的角度来看都是原子的。 也就是说,对高 32 位和低 32 位的更新不能被破坏,并且应该同时可见。 此概念已存在于现有的受监视围栏对象中。
-
受监视的值(本文中称为 MonitoredValue)。 此内存位置包含 CPU 减去 1(1)的值当前等待的最小值。 MonitoredValue 被映射到 CPU 并可访问 CPU(可从内核模式读取和可写入、无用户模式访问)和 GPU(使用 GPU VA 读取,无写入访问权限)。 OS 维护给定围栏对象的未完成 CPU 等待程序列表,并在添加和删除等待程序时更新 MonitoredValue。 当没有未完成的等待程序时,该值将设置为 UINT64_MAX。 此概念是 GPU 本机围栏同步对象的新增概念。
下图说明了 Dxgkrnl 如何在特定的受监视围栏值上跟踪未完成的 CPU 等待程序。 同时还显示了在给定时间点设置的受监视围栏值。 CurrentValue 和 MonitoredValue 均为 41,这意味着:
- GPU 完成了围栏值 41 之前的所有任务。
- CPU 未等待任何小于或等于 41 的围栏值。
下图说明了 GPU 的上下文管理处理器 (CMP) 只有在新围栏值大于受监视值时才有条件地引发 CPU 中断。 这种中断意味着有未完成的 CPU 等待程序可以满足新写入的值。
当 CPU 处理此中断时,Dxgkrnl 将执行如下图所示的操作:
- 它解除对新编写的围栏感到满意的 CPU 等待程序的锁定。
- 它将受监视的值向前推进,使其对应于等待时间最少的值减去 1。
用于当前值和受监视围栏值的物理内存存储
对于给定的围栏对象,CurrentValue 和 MonitoredValue 存储在不同的位置。
-
不可共享的围栏对象在同一内存页中为同一进程中的不同围栏对象存储围栏值。 这些值根据本文稍后所述的本机围栏 KMD 上限中指定的步幅值进行打包。
-
可共享的围栏对象将其当前值和受监视值放置在不与其他围栏对象共享的内存页中。
当前值
当前值可以驻留在系统内存或 GPU 本地内存中,具体取决于D3DDDI_NATIVEFENCE_TYPE指定的围栏类型。
- 跨适配器围栏的当前值始终位于系统内存中。
- 将当前值存储在系统内存中时,将从内部系统内存池分配存储。
- 将当前值存储在本地内存中时,将从驱动程序在D3DKMDT_FENCESTORAGESURFACEDATA中指定的内存段分配存储。
受监视的值
监视的值也可以驻留在系统或 GPU 本地内存中,具体取决于 D3DDDI_NATIVEFENCE_TYPE。
- 当受监视的值存储在系统内存中时,OS 将从内部系统内存池分配存储。
- 当受监视的值存储在本地内存中时,OS 将从D3DKMDT_FENCESTORAGESURFACEDATA中指定的驱动程序的内存段分配存储。
当 OS 的 CPU 等待条件发生更改时,它会调用 KMD 的 DxgkDdiUpdateMonitoredValues 回调,以指示 KMD 将监视的值更新为指定的值。
同步问题
上述机制在 CPU 和 GPU 读取和写入当前值与受监视的值之间具有固有的争用条件。 如果不特别注意,可能会出现以下问题:
- GPU 可以读取过时的 MonitoredValue,而不会像 CPU 预期的那样引发中断。
- GPU 引擎可以在 CMP 决定中断条件的过程中写入一个较新的 CurrentValue。 此较新的 CurrentValue 可能不会按预期引发中断,也可能在 CPU 获取当前值时不可见。
GPU 内引擎与 CMP 之间的同步
为了提高效率,许多离散 GPU 使用驻留在 GPU 本地内存中的阴影状态实现受监视的围栏信号语义:
-
GPU 引擎执行命令缓冲区流,并有条件地向 CMP 发出硬件信号。
-
决定是否应引发 CPU 中断的 GPU CMP。
在这种情况下,CMP 需要将内存访问与执行对围栏值的内存写入的 GPU 引擎同步。 具体而言,应从 CMP 角度对更新阴影 MonitoredValue 的操作进行排序:
- 编写新的 MonitoredValue (影子 GPU 存储)。
- 执行内存屏障以将内存访问与 GPU 引擎同步。
- 读取 CurrentValue:
- 如果 CurrentValue>MonitoredValue,则引发 CPU 中断。
- 如果 CurrentValue<= MonitoredValue,则不引发 CPU 中断。
为了正确解决这种争用条件,步骤 2 中的内存屏障必须正常运行。 在步骤 3 中,如果命令没有看到步骤 1 中 MonitoredValue 的更新,则不能出现对 CurrentValue 的内存写入操作。 如果步骤 3 中写入的围栏大于步骤 1 中更新的值,则这种情况将生成中断。