用户模式工作提交

重要

一些信息与预发布产品相关,在商业发行之前可能会发生实质性修改。 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)期间, DxgkrnlDoorbellCpuVirtualAddress 返回到 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 可以使用挂起的上下文调用 D3DKMTSubmitCommandDxgkrnl 将这个新命令排队到 HwQueue,但要等到以后才会被调度。

以下事件序列在硬件上下文挂起和恢复期间发生。

  • 暂停硬件上下文:

    1. Dxgkrnl 调用 DxgkddiSuspendContext
    2. KMD 从硬件调度程序的列表中移除上下文中的所有硬件队列。
    3. 门铃仍然连接,环形缓冲区/环形缓冲区控制分配仍然驻留。 UMD 可以将新命令写入此上下文的 HWQueue,但 GPU 不会处理它们,这类似于当前内核模式下命令提交到被挂起的上下文。
    4. 如果 KMD 选择损害挂起的 HWQueue 的门铃,则 UMD 将失去连接。 UMD 可以尝试重新连接门铃,KMD 会将新的门铃分配给此队列。 意图不是停止 UMD,而是允许它继续提交工作,以便 HW 引擎在上下文恢复后能够最终处理这些工作。
  • 还原硬件上下文状态

    1. Dxgkrnl 调用 DxgkddiResumeContext
    2. 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 -> TransitionToF1Active -> 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 电源状态转换:

    1. Dxgkrnl 为 GPU 上的所有 HW 上下文调用 DxgkddiSuspendContext 。 KMD 从硬件调度程序列表中删除这些上下文。
    2. Dxgkrnl 断开所有门铃的连接。
    3. 如果需要,Dxgkrnl 可能会从 VRAM 中逐出所有环形缓冲区/环形缓冲区控制的内存分配。 在所有上下文都被挂起并从硬件调度器列表中移除之后,系统这样做是为了确保硬件不再引用任何被逐出的内存。
  • 当 GPU 处于 D3 状态时,UMD 会将新命令写入 HWQueue:

    1. UMD 注意到门铃连接已断开,因此调用 D3DKMTConnectDoorbell
    2. Dxgkrnl 启动 D0 转换。
    3. Dxgkrnl 将所有环形缓冲区/环形缓冲区控制分配设为常驻(如果它们被逐出)。
    4. Dxgkrnl 调用 KMD 的 DxgkddiCreateDoorbell 函数,以请求 KMD 为此 HWQueue 建立门铃连接。
    5. Dxgkrnl 为所有 HWContext 调用 DxgkddiResumeContext 。 KMD 将相应的队列添加到硬件调度程序的列表中。

用于用户模式工作提交的设备驱动接口 (DDI)

KMD 实现的 DDI

为 KMD 添加了以下内核模式 DDI,以实现用户模式工作提交支持。

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:

  1. Dxgkrnl 调用 DxgkDdiDisconnectDoorbell 以断开门铃。
  2. Dxgkrnl 等待最后排队的 HwQueueProgressFenceLastQueuedValueCPUVirtualAddress 在 GPU 上完成。 环形缓冲区/环形缓冲区控制分配保持不变。
  3. Dxgkrnl 的等待已得到满足,现在可以销毁环形缓冲区/环缓冲区控制分配以及门铃和 HWQueue 对象。

以下事件序列在异常进程终止期间发生。

  1. Dxgkrnl 标记出错的设备。

  2. 对于每个设备上下文,Dxgkrnl 会调用 DxgkddiSuspendContext 来暂停该上下文。 环形缓冲区/环形缓冲区控制的分配仍然常驻。 KMD 抢占上下文,并将其从其 HW 运行列表中删除。

  3. 对于上下文的每个 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 调用 DxgkrnlDxgkCbDisconnectDoorbellCB(hDoorbell1->hHwQueue)
    • Dxgkrnl 将此门铃的 UMD 可见 CPUVA 旋转到虚拟页面,并将状态值更新为D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY。
  • KMD 恢复控制权,并实际执行受害行为/断开连接。
    • KMD 使 hDoorbell1 成为受害者,并将其与 PhysicalDoorbell1 断开连接。
    • PhysicalDoorbell1 可供使用

现在,请考虑以下方案:

  1. 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
    
  2. 操作系统调用 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 hDoorbell1
    
  3. OS 在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 使用全局门铃,则不需要此机制。 相反,在此示例中,hDoorbell1hDoorbell2 都将被分配到相同的 0xfeedfeee 物理门铃。