重要
一些信息与预发布产品相关,在商业发行之前可能会发生实质性修改。 Microsoft对此处提供的信息不作任何明示或暗示的保证。
本文介绍在 Windows 11 版本 24H2(WDDM 3.2)中仍在开发的用户模式(UM)工作提交功能。 UM 工作提交使应用程序能够直接从用户模式将工作提交到 GPU,延迟非常低。 目标是提高经常将小型工作负荷提交到 GPU 的应用程序的性能。 此外,如果用户模式提交在容器或虚拟机(VM)内运行,则这些应用程序将大大受益。 此优点是,VM 中运行的用户模式驱动程序(UMD)可以直接将工作提交到 GPU,而无需向主机发送消息。
支持 UM 工作提交的 IHV 驱动程序和硬件必须继续支持传统的内核模式工作提交模型。 对于诸如仅支持传统 KM 队列且运行在最新主机上的较旧虚拟机等场景,需要此支持。
本文不讨论 UM 提交与 Flip/FlipEx 的互作性。 本文中所述的 UM 提交仅限于仅呈现/计算的场景类别。 呈现管道目前仍然基于内核模式提交,因为它依赖于本地监控围栏。 一旦本机监控围栏和仅用于计算/呈现的 UM 提交完全实现,就可以考虑 UM 提交相关演示文稿的设计和实现。 因此,驱动程序应支持按队列提交用户模式。
门铃
支持硬件计划的大部分最新或即将推出的 GPU 也支持 GPU 门铃的概念。 门铃是一种机制,用于通知 GPU 引擎,工作队列中有新的任务已排队。 门铃通常寄存于 PCIe BAR(基地址寄存器)或系统内存中。 每个 GPU IHV 都有自己的体系结构,用于确定门铃的数量、它们位于系统中的位置,等等。 Windows OS 使用门铃作为其设计的一部分来实现 UM 工作提交。
在高级别上,有两种不同的门铃模型由不同的 IHV 和 GPU 实现:
全球门铃
在 Global Doorbells 模型中,上下文和进程中的所有硬件队列共享单个全局门铃。 写入 Doorbell 的值告知 GPU 调度器哪个特定的硬件队列和引擎有新任务。 如果多个硬件队列同时提交工作并使用同一个全局信号,GPU 硬件通过一种轮询机制来提取工作。
专用门铃
在专用门铃模型中,每当有新的工作提交到 GPU 时,都会为每个硬件队列分配自己的门铃。 当门铃运行时,GPU 计划程序确切地知道哪个硬件队列提交了新工作。 在由 GPU 创建的所有硬件队列中,共用数量有限的门铃。 如果创建的硬件队列数超过可用门铃的数量,则驱动程序需要断开较旧或最近使用最少的硬件队列的门铃的连接,并将其门铃分配给新创建的队列,从而有效地“虚拟化”门铃。
发现用户模式工作提交支持功能
DXGK_NODEMETADATA_FLAGS::UserModeSubmissionSupported
对于支持 UM 工作提交功能的 GPU 节点,KMD 的 DxgkDdiGetNodeMetadata 会设置 UserModeSubmissionSupported 节点元数据标志,并将其添加到 DXGK_NODEMETADATA_FLAGS 中。 然后,OS 允许 UMD 仅在设置了此标志的节点上创建用户模式提交的 HWQueues 和门铃。
DXGK_QUERYADAPTERINFOTYPE::DXGKQAITYPE_USERMODESUBMISSION_CAPS
为了查询门铃特定信息,操作系统会使用KMD 的 DxgkDdiQueryAdapterInfo函数,并通过DXGKQAITYPE_USERMODESUBMISSION_CAPS查询适配器信息类型。 KMD 通过填充DXGK_USERMODESUBMISSION_CAPS结构,以提供其对用户模式工作提交的支持详细信息来响应。
目前,唯一需要的上限是门铃内存大小(以字节为单位)。 Dxgkrnl 出于以下几个原因需要门铃内存大小:
- 在门铃创建(D3DKMTCreateDoorbell)期间, Dxgkrnl 将 DoorbellCpuVirtualAddress 返回到 UMD。 在执行此作之前, Dxgkrnl 首先需要内部映射到虚拟页面,因为门铃尚未分配并连接。 为了分配虚拟页面,需要门铃的大小。
 - 在门铃连接(D3DKMTConnectDoorbell)期间, Dxgkrnl 需要将 DoorbellCpuVirtualAddress 旋转到 KMD 提供的 DoorbellPhysicalAddress 。 同样, Dxgkrnl 需要知道门铃大小。
 
D3DDDI_CREATEHWQUEUEFLAGS::D3DKMTCreateHwQueue 中的 UserModeSubmission
UMD 设置 UserModeSubmission 标志并将其添加到 D3DDDI_CREATEHWQUEUEFLAGS,以创建使用用户模式提交模型的 HWQueues。 使用此标志创建的 HWQueues 无法使用常规内核模式的工作提交路径,工作提交必须依赖队列上的门铃机制。
用户模式工作提交 API
添加了以下用户模式 API 以支持用户模式工作提交。
D3DKMTCreateDoorbell 用于为用户模式工作提交创建一个 D3D HWQueue 的门铃。
D3DKMTConnectDoorbell 将之前创建的门铃连接到用于提交用户模式工作的 D3D 硬件队列 (HWQueue)。
D3DKMTDestroyDoorbell 销毁以前创建的门铃。
D3DKMTNotifyWorkSubmission 通知 KMD 在 HWQueue 上提交了新工作。 此功能的要点是低延迟工作提交路径,其中 KMD 不涉及或知道何时提交工作。 此 API 适用于在 HWQueue 上提交工作时需要通知 KMD 的情况。 驱动程序应在特定和不频繁的情况下使用此机制,因为它涉及每次工作提交时从 UMD 到 KMD 的往返,从而破坏了低延迟用户模式提交模型的目的。
门铃内存和环形缓冲区分配的驻留模型
- UMD 负责在创建门铃之前使环形缓冲区和环形缓冲区控制分配驻留。
 - UMD 管理环形缓冲区和环形缓冲区控制分配的生存期。 Dxgkrnl 不会隐式销毁这些分配,即使相应的门铃被销毁。 UMD 负责分配和销毁这些分配。 但是,为了防止恶意用户态程序在门铃处于活动状态时销毁这些分配,Dxgkrnl 在门铃的生存期内确实会对这些分配进行引用。
 - Dxgkrnl 销毁环缓冲区分配的唯一方案是在设备终止期间。 Dxgkrnl 销毁与设备关联的所有 HWQueues、门铃和环形缓冲区分配。
 - 只要环形缓冲区分配仍然存在,环形缓冲区的 CPUVA 始终有效,并可供 UMD 访问,而无需考虑门铃连接的状态。 也就是说,环形缓冲区驻留与门铃无关。
 - 当 KMD 进行 DXG 回调以断开门铃连接(即调用 DxgkCbDisconnectDoorbell,并状态为 D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY)时,Dxgkrnl 会将门铃的 CPUVA 映射到一个虚拟页。 它不会清除或取消内存映射环形缓冲区分配。
 - 如果出现任何设备丢失的情况(如TDR/GPU停止/页面错误等),Dxgkrnl 会断开与门铃的连接,并将状态标记为D3DDDI_DOORBELL_STATUS_DISCONNECTED_ABORT。 用户模式负责销毁 HWQueue、门铃、环形缓冲区,并重新创建它们。 此要求类似于在此方案中销毁和重新创建其他设备资源的方式。
 
硬件上下文挂起
当 OS 暂停硬件上下文时,Dxgkrnl 继续保持门铃连接处于活动状态,并确保环形缓冲区(用于工作队列)的分配始终驻留。 通过这种方式,UMD 可以继续将工作加入上下文队列,但当上下文暂停时,这些工作不会被安排。 上下文被恢复并进行调度后,GPU 的上下文管理处理器(CMP)将观察新的写入指针和工作提交。
此逻辑类似于当前的内核模式提交逻辑,其中 UMD 可以使用挂起的上下文调用 D3DKMTSubmitCommand 。 Dxgkrnl 将这个新命令排队到 HwQueue,但要等到以后才会被调度。
以下事件序列在硬件上下文挂起和恢复期间发生。
暂停硬件上下文:
- Dxgkrnl 调用 DxgkddiSuspendContext。
 - KMD 从硬件调度程序的列表中移除上下文中的所有硬件队列。
 - 门铃仍然连接,环形缓冲区/环形缓冲区控制分配仍然驻留。 UMD 可以将新命令写入此上下文的 HWQueue,但 GPU 不会处理它们,这类似于当前内核模式下命令提交到被挂起的上下文。
 - 如果 KMD 选择损害挂起的 HWQueue 的门铃,则 UMD 将失去连接。 UMD 可以尝试重新连接门铃,KMD 会将新的门铃分配给此队列。 意图不是停止 UMD,而是允许它继续提交工作,以便 HW 引擎在上下文恢复后能够最终处理这些工作。
 
还原硬件上下文状态
- Dxgkrnl 调用 DxgkddiResumeContext。
 - KMD 将上下文的所有硬件队列添加到硬件调度程序的列表。
 
引擎 F 状态转换
在传统的内核模式工作提交中,Dxgkrnl 负责将新命令提交到“HWQueue”,并监视来自 KMD 的完成中断。 因此, Dxgkrnl 具有引擎处于活动状态和空闲时间的完整视图。
在用户模式工作提交中,Dxgkrnl 使用 TDR 超时节奏监视 GPU 引擎是否有进展,因此,如果值得在两秒的 TDR 超时时间之前启动转换到 F1 状态,KMD 可以请求 OS 执行这一操作。
进行了以下更改,以促进此方法:
DXGK_INTERRUPT_GPU_ENGINE_STATE_CHANGE中断类型已添加到DXGK_INTERRUPT_TYPE。 KMD 使用此中断通知 Dxgkrnl 当引擎状态转换需要 GPU 电源动作或超时恢复时,例如 Active -> TransitionToF1 和 Active -> Hung。
EngineStateChange 中断数据结构已添加到DXGKARGCB_NOTIFY_INTERRUPT_DATA。
添加了 DXGK_ENGINE_STATE 枚举来表示 EngineStateChange 的引擎状态转换。
当 KMD 引发 DXGK_INTERRUPT_GPU_ENGINE_STATE_CHANGE 中断,且 EngineStateChange.NewState 设置为 DXGK_ENGINE_STATE_TRANSITION_TO_F1 时,Dxgkrnl 会断开连接此引擎上 HWQueues 的所有门铃,然后启动从 F0 到 F1 的电源组件转换。
当 UMD 尝试以 F1 状态将新工作提交到 GPU 引擎时,需要重新连接门铃,这反过来又会导致 Dxgkrnl 启动转换回 F0 电源状态。
发动机 D 状态转换
在 D0 到 D3 设备电源状态转换期间,Dxgkrnl 挂起 HWQueue,断开门铃(将门铃 CPUVA 旋转到虚设页面),并将 DoorbellStatusCpuVirtualAddress 门铃状态更新为 D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY。
如果 UMD 在 GPU 处于 D3 时调用 D3DKMTConnectDoorbell ,则会强制 Dxgkrnl 将 GPU 唤醒为 D0。 Dxgkrnl 还负责恢复 HWQueue 并将门铃 CPUVA 旋转到物理门铃位置。
将发生以下事件序列。
发生 GPU 从 D0 到 D3 电源状态转换:
- Dxgkrnl 为 GPU 上的所有 HW 上下文调用 DxgkddiSuspendContext 。 KMD 从硬件调度程序列表中删除这些上下文。
 - Dxgkrnl 断开所有门铃的连接。
 - 如果需要,Dxgkrnl 可能会从 VRAM 中逐出所有环形缓冲区/环形缓冲区控制的内存分配。 在所有上下文都被挂起并从硬件调度器列表中移除之后,系统这样做是为了确保硬件不再引用任何被逐出的内存。
 
当 GPU 处于 D3 状态时,UMD 会将新命令写入 HWQueue:
- UMD 注意到门铃连接已断开,因此调用 D3DKMTConnectDoorbell。
 - Dxgkrnl 启动 D0 转换。
 - Dxgkrnl 将所有环形缓冲区/环形缓冲区控制分配设为常驻(如果它们被逐出)。
 - Dxgkrnl 调用 KMD 的 DxgkddiCreateDoorbell 函数,以请求 KMD 为此 HWQueue 建立门铃连接。
 - Dxgkrnl 为所有 HWContext 调用 DxgkddiResumeContext 。 KMD 将相应的队列添加到硬件调度程序的列表中。
 
用于用户模式工作提交的设备驱动接口 (DDI)
KMD 实现的 DDI
为 KMD 添加了以下内核模式 DDI,以实现用户模式工作提交支持。
DxgkDdiCreateDoorbell。 当 UMD 调用 D3DKMTCreateDoorbell 为 HWQueue 创建门铃时, Dxgkrnl 会对此函数进行相应的调用,以便 KMD 可以初始化其门铃结构。
DxgkDdiConnectDoorbell。 当 UMD 调用 D3DKMTConnectDoorbell 时, Dxgkrnl 会对此函数进行相应的调用,以便 KMD 可以提供映射到物理门铃位置的 CPUVA,并建立 HWQueue 对象、门铃对象、门铃物理地址、GPU 计划程序等所需的连接。
DxgkDdiDisconnectDoorbell。 当 OS 想要断开特定门铃的连接时,它会使用此 DDI 调用 KMD。
DxgkDdiDestroyDoorbell。 当 UMD 调用 D3DKMTDestroyDoorbell 时, Dxgkrnl 对此函数进行相应的调用,以便 KMD 可以销毁其门铃结构。
DxgkDdiNotifyWorkSubmission。 当 UMD 调用 D3DKMTNotifyWorkSubmission 时, Dxgkrnl 对此函数发出相应的调用,以便可以通知 KMD 新工作提交。
Dxgkrnl 实现的 DDI
DxgkCbDisconnectDoorbell 回调由 Dxgkrnl 实现。 KMD 可以调用此函数来通知 Dxgkrnl KMD 需要断开特定门铃的连接。
HW 队列进度栅栏更改
在 UM 工作提交模型中运行的硬件队列仍然具有一个由 UMD 在命令缓冲区完成时生成和写入的单调递增的进度围栏值的概念。 UMD 需要在将新的指令缓冲区追加到环形缓冲区并使其对 GPU 可见之前,更新该队列的进度围栏值,以便让 Dxgkrnl 知道特定硬件队列是否有挂起的工作。 CreateDoorbell.HwQueueProgressFenceLastQueuedValueCPUVirtualAddress 是最新排队值读取/写入用户模式进程映射。
UMD 必须确保在新提交对 GPU 可见之前立即更新排队值。 以下步骤是建议的操作顺序。 它们假定 HW 队列处于空闲状态,最后一个完成的缓冲区的进度栅栏值为 N。
- 生成新的进度围栏值 N+1。
 - 填写命令缓冲区。 命令缓冲区的最后一个指令是写入 N+1 的进度围栏值。
 - 将 *(HwQueueProgressFenceLastQueuedValueCPUVirtualAddress) 设置为 N+1,通知操作系统新排队的值。
 - 通过将命令缓冲区添加到环形缓冲区,使命令缓冲区对 GPU 可见。
 - 按门铃。
 
正常和异常进程终止
以下事件序列在正常进程终止期间发生。
对于每个设备/上下文的 HWQueue:
- Dxgkrnl 调用 DxgkDdiDisconnectDoorbell 以断开门铃。
 - Dxgkrnl 等待最后排队的 HwQueueProgressFenceLastQueuedValueCPUVirtualAddress 在 GPU 上完成。 环形缓冲区/环形缓冲区控制分配保持不变。
 - Dxgkrnl 的等待已得到满足,现在可以销毁环形缓冲区/环缓冲区控制分配以及门铃和 HWQueue 对象。
 
以下事件序列在异常进程终止期间发生。
Dxgkrnl 标记出错的设备。
对于每个设备上下文,Dxgkrnl 会调用 DxgkddiSuspendContext 来暂停该上下文。 环形缓冲区/环形缓冲区控制的分配仍然常驻。 KMD 抢占上下文,并将其从其 HW 运行列表中删除。
对于上下文的每个 HWQueue,Dxglrnl:
a。 调用 DxgkDdiDisconnectDoorbell 断开门铃。
b. 销毁环形缓冲区、环形缓冲区控制的分配对象,以及门铃和 HWQueue 对象。
伪代码示例
UMD 中的工作提交伪代码
以下伪代码是一个基本示例,展示了 UMD 应如何使用门铃 API 为 HWQueues 创建和提交工作。 请注意 hHWqueue1 是使用现有 D3DKMTCreateHwQueue API 并通过 UserModeSubmission 标志创建的 HWQueue 的句柄。
// Create a doorbell for the HWQueue
D3DKMT_CREATE_DOORBELL CreateDoorbell = {};
CreateDoorbell.hHwQueue = hHwQueue1;
CreateDoorbell.hRingBuffer = hRingBufferAlloc;
CreateDoorbell.hRingBufferControl = hRingBufferControlAlloc;
CreateDoorbell.Flags.Value = 0;
NTSTATUS ApiStatus =  D3DKMTCreateDoorbell(&CreateDoorbell);
if(!NT_SUCCESS(ApiStatus))
  goto cleanup;
assert(CreateDoorbell.DoorbellCPUVirtualAddress!=NULL && 
      CreateDoorbell.DoorbellStatusCPUVirtualAddress!=NULL);
// Get a CPUVA of Ring buffer control alloc to obtain write pointer.
// Assume the write pointer is at offset 0 in this alloc
D3DKMT_LOCK2 Lock = {};
Lock.hAllocation = hRingBufferControlAlloc;
ApiStatus = D3DKMTLock2(&Lock);
if(!NT_SUCCESS(ApiStatus))
  goto cleanup;
UINT64* WritePointerCPUVirtualAddress = (UINT64*)Lock.pData;
// Doorbell created successfully. Submit command to this HWQueue
UINT64 DoorbellStatus = 0;
do
{
  // first connect the doorbell and read status
  ApiStatus = D3DKMTConnectDoorbell(hHwQueue1);
  D3DDDI_DOORBELL_STATUS DoorbellStatus = *(UINT64*(CreateDoorbell.DoorbellStatusCPUVirtualAddress));
  if(!NT_SUCCESS(ApiStatus) ||  DoorbellStatus == D3DDDI_DOORBELL_STATUS_DISCONNECTED_ABORT)
  {
    // fatal error in connecting doorbell, destroy this HWQueue and re-create using traditional kernel mode submission.
    goto cleanup_fallback;
  }
  // update the last queue progress fence value
  *(CreateDoorbell.HwQueueProgressFenceLastQueuedValueCPUVirtualAddress) = new_command_buffer_progress_fence_value;
  // write command to ring buffer of this HWQueue
  *(WritePointerCPUVirtualAddress) = address_location_of_command_buffer;
  // Ring doorbell by writing the write pointer value into doorbell address. 
  *(CreateDoorbell.DoorbellCPUVirtualAddress) = *WritePointerCPUVirtualAddress;
  // Check if submission succeeded by reading doorbell status
  DoorbellStatus = *(UINT64*(CreateDoorbell.DoorbellStatusCPUVirtualAddress));
  if(DoorbellStatus == D3DDDI_DOORBELL_STATUS_CONNECTED_NOTIFY)
  {
      D3DKMTNotifyWorkSubmission(CreateDoorbell.hDoorbell);
  }
} while (DoorbellStatus == D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY);
在 KMD 中使门铃伪代码受害
以下示例说明了 KMD 如何将使用专用门铃的 GPU 上的 HWQueues“虚拟化”,并在它们之间共享可用的门铃。
KMD 函数 VictimizeDoorbell() 的伪代码:
- KMD 决定逻辑门铃
hDoorbell1连接到PhysicalDoorbell1需要被停用和断开连接。 - KMD 调用 Dxgkrnl 的 
DxgkCbDisconnectDoorbellCB(hDoorbell1->hHwQueue)。- Dxgkrnl 将此门铃的 UMD 可见 CPUVA 旋转到虚拟页面,并将状态值更新为D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY。
 
 - KMD 恢复控制权,并实际执行受害行为/断开连接。
- KMD 使 
hDoorbell1成为受害者,并将其与PhysicalDoorbell1断开连接。 - 
              
PhysicalDoorbell1可供使用 
 - KMD 使 
 
现在,请考虑以下方案:
PCI BAR 中有一个物理门铃,其内核模式 CPUVA 等于
0xfeedfeee。 为 HWQueue 创建的门铃对象分配此物理门铃值。HWQueue KMD Handle: hHwQueue1 Doorbell KMD Handle: hDoorbell1 Doorbell CPU Virtual Address: CpuVirtualAddressDoorbell1 => 0xfeedfeee // hDoorbell1 is mapped to 0xfeedfeee Doorbell Status CPU Virtual Address: StatusCpuVirtualAddressDoorbell1 => D3DDDI_DOORBELL_STATUS_CONNECTED操作系统调用
DxgkDdiCreateDoorbell以用于不同的HWQueue2:HWQueue KMD Handle: hHwQueue2 Doorbell KMD Handle: hDoorbell2 Doorbell CPU Virtual Address: CpuVirtualAddressDoorbell2 => 0 // this doorbell object isn't yet assigned to a physical doorbell Doorbell Status CPU Virtual Address: StatusCpuVirtualAddressDoorbell2 => D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY // In the create doorbell DDI, KMD doesn't need to assign a physical doorbell yet, // so the 0xfeedfeee doorbell is still connected to hDoorbell1OS 在
hDoorbell2上调用DxgkDdiConnectDoorbell// KMD needs to victimize hDoorbell1 and assign 0xfeedfeee to hDoorbell2. VictimizeDoorbell(hDoorbell1); // Physical doorbell 0xfeedfeee is now free and can be used vfor hDoorbell2. // KMD makes required connections for hDoorbell2 with HW ConnectPhysicalDoorbell(hDoorbell2, 0xfeedfeee) return 0xfeedfeee // On return from this DDI, *Dxgkrnl* maps 0xfeedfeee to process address space CPUVA i.e: // CpuVirtualAddressDoorbell2 => 0xfeedfeee // *Dxgkrnl* updates hDoorbell2 status to connected i.e: // StatusCpuVirtualAddressDoorbell2 => D3DDDI_DOORBELL_STATUS_CONNECTED ``
如果 GPU 使用全局门铃,则不需要此机制。 相反,在此示例中,hDoorbell1 和 hDoorbell2 都将被分配到相同的 0xfeedfeee 物理门铃。