共用方式為


使用者模式作業提交

這很重要

某些資訊與一款尚未正式發行的產品有關,該產品在正式推出之前可能會進行大幅修改。 Microsoft 對於此處提供的資訊,不做任何明確或隱含的保證。

本文說明使用者模式 (UM) 工作提交功能,該功能仍處於 Windows 11 版本 24H2 (WDDM 3.2) 的開發中。 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 實作:

  • 全球門鈴

    在全域門鈴模型中,跨環境定義和進程的所有硬體佇列都會共用單一全域門鈴。 寫入門鈴的值會通知 GPU 排程器哪個特定硬體佇列和引擎有新工作。 如果多個硬體佇列主動提交工作並按響相同的全域門鈴,GPU 硬體會使用輪詢機制的形式來擷取工作。

  • 專屬門鈴

    在專用門鈴模型中,每個硬體佇列都會指派自己的門鈴,每當有新工作要提交給 GPU 時,門鈴就會響起。 當門鈴響起時,GPU 排程器會確切地知道哪個硬體佇列提交了新工作。 有數量有限的門鈴被共享於 GPU 上所建立的所有硬體佇列之間。 如果建立的硬體佇列數目超過可用的門鈴數目,驅動程式必須中斷較舊或最近最少使用的硬體佇列的門鈴,並將其門鈴指派給新建立的佇列,以有效地「虛擬化」門鈴。

探索使用者模式工作提交支援

DXGK_NODEMETADATA_FLAGS::支援使用者模式提交

對於支援 UM 工作提交功能的 GPU 節點,KMD 的 DxgkDdiGetNodeMetadata 會設定 UserModeSubmissionSupported 節點中繼資料旗標,並將其新增至 DXGK_NODEMETADATA_FLAGS。 然後,OS 允許 UMD 僅在設定了此旗標的節點上建立使用者模式提交的 HWQueues 和門鈴。

DXGK_QUERYADAPTERINFOTYPE::DXGKQAITYPE_USERMODESUBMISSION_CAPS

若要查詢門鈴特定資訊,OS 會呼叫 KMD 的 DxgkDdiQueryAdapterInfo 函式,其中包含 DXGKQAITYPE_USERMODESUBMISSION_CAPS 查詢配接器資訊類型。 KMD 會以使用者模式工作提交的支援詳細資料填入 DXGK_USERMODESUBMISSION_CAPS 結構來回應。

目前,唯一需要的上限是門鈴記憶體大小(以位元組為單位)。 Dxgkrnl 需要門鈴記憶體大小,原因有幾個:

  • 在建立門鈴期間 (D3DKMTCreateDoorbell) , Dxgkrnl 會將 DoorbellCpuVirtualAddress 傳回給 UMD。 在執行此動作之前, Dxgkrnl 首先需要在內部映射到虛擬頁面,因為門鈴尚未分配和連接。 需要門鈴的大小來分配虛擬頁面。
  • 在門鈴連線期間 (D3DKMTConnectDoorbell) , Dxgkrnl 需要將 DoorbellCpuVirtualAddress 輪換為 KMD 所提供的 DoorbellPhysicalAddress 。 同樣, Dxgkrnl 需要知道門鈴尺寸。

D3DDDI_CREATEHWQUEUEFLAGS::UserModeSubmission 在 D3DKMTCreateHwQueue 中

UMD 會設定新增 UserModeSubmission 旗標至 D3DDDI_CREATEHWQUEUEFLAGS,以建立使用者模式提交模型的 HWQueue。 使用此旗標建立的 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 會摧毀與裝置相關聯的所有 HWQueue、門鈴和環形緩衝區配置。
  • 只要環形緩衝區配置處於作用中狀態,環形緩衝區 CPUVA 一律有效,且可供 UMD 存取,而不論門鈴連線狀態為何。 也就是說,環緩衝區駐留與門鈴無關。
  • 當 KMD 執行 DXG 回呼以斷開門鈴的連線(即調用狀態為 D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY 的 DxgkCbDisconnectDoorbell 時),Dxgkrnl 會將門鈴的 CPUVA 旋轉至一個虛擬頁面。 它不會收回或取消對應環緩衝區配置。
  • 如果發生任何裝置失效情況(TDR/GPU 停止/分頁等),Dxgkrnl 會斷開門鈴並將狀態標記為 D3DDDI_DOORBELL_STATUS_DISCONNECTED_ABORT。 使用者模式負責銷毀 HWQueue、門鈴、響鈴緩衝區,並重新建立它們。 此需求類似於在此案例中銷毀和重新建立其他裝置資源的方式。

硬體上下文暫停

當 OS 暫停硬體上下文時, Dxgkrnl 會讓門鈴連線保持作用中,並保留響鈴緩衝區(工作佇列)配置。 如此一來,UMD 就可以繼續將作業加入上下文的佇列,不過在上下文暫停時,該作業不會排程。 一旦上下文恢復並完成排程,GPU 的上下文管理處理器(CMP)就會觀察新的寫入指標和工作提交。

此邏輯類似於目前的核心模式的提交邏輯,其中 UMD 可以使用暫停的上下文呼叫 D3DKMTSubmitCommandDxgkrnl 會將此新命令排入佇列至 HwQueue,但直到稍後才會被排程執行。

下列事件序列會在硬體環境暫停和繼續期間發生。

  • 暫停硬體上下文:

    1. Dxgkrnl 呼叫 DxgkddiSuspendContext
    2. KMD 會從硬體排程器的清單中移除上下文的所有 HWQueues。
    3. 門鈴仍處於連接狀態,且響鈴緩衝區/環形緩衝區控制分配仍常駐。 UMD 可以將新命令寫入該上下文的 HWQueue,但 GPU 不會處理這些命令,這類似於現今的核心模式命令提交到被暫停的上下文。
    4. 如果 KMD 選擇傷害暫停的 HWQueue 的門鈴,那麼 UMD 就會失去連接。 UMD 可以嘗試重新連線門鈴,KMD 會將新的門鈴指派給此佇列。 目的是不要暫停 UMD,而是允許它在恢復運行環境後繼續提交硬體引擎最終能夠處理的工作。
  • 恢復硬體上下文:

    1. Dxgkrnl 呼叫 DxgkddiResumeContext
    2. KMD 會將上下文的所有 HWQueues 新增到硬體排程器的清單中。

發動機 F 狀態轉換

在傳統核心模式的工作提交中,Dxgkrnl 負責將新命令提交至 HWQueue,並監視來自 KMD 的完成中斷訊號。 因此, Dxgkrnl 可以完整地了解引擎何時處於活動狀態和怠速狀態。

在使用者模式工作提交中, Dxgkrnl 會監視 GPU 引擎是否使用 TDR 逾時步調進行進度,因此如果值得比兩秒 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 輪換到物理門鈴位置。

會發生下列一系列事件。

  • 發生 D0 至 D3 GPU 斷電:

    1. Dxgkrnl 會針對 GPU 上的所有硬體內容呼叫 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 系統

已新增下列核心模式 DDI,讓 KMD 實作使用者模式工作提交支援。

Dxgkrnl 實現的 DDI

DxgkCbDisconnectDoorbell 回呼是由 Dxgkrnl 實作。 KMD 可以呼叫此函式來通知 Dxgkrnl KMD 需要中斷特定門鈴的連線。

硬體排程進度屏障變更

在 UM 工作提交模型中執行的硬體佇列仍然具有單調遞增進度柵欄值的概念,UMD 會在命令緩衝區完成時產生並寫入。 為了讓 Dxgkrnl 知道特定硬體佇列是否有擱置的工作,UMD 必須先更新佇列的進度柵欄值,再將新的命令緩衝區附加至環形緩衝區,並讓 GPU 可見。 CreateDoorbell.HwQueueProgressFenceLastQueuedValueCPUVirtualAddress 是最新佇列值的讀取/寫入使用者模式進程對應。

UMD 必須確保在新提交在 GPU 上可見之前立即更新佇列值。 下列步驟是建議的作業順序。 他們假設硬體佇列處於閒置狀態,且最後完成的緩衝區的進度柵欄值為 N

  • 產生新的進度柵欄值 N+1
  • 填充命令緩衝區。 命令緩衝區的最後一條指令是寫入 N+1 的進度柵欄值。
  • 將 *(HwQueueProgressFenceLastQueuedValueCPUVirtualAddress) 設定為 N+1,以便將最新佇列的值通知給 OS。
  • 將命令緩衝區新增至環形緩衝區,讓命令緩衝區對 GPU 可見。
  • 按門鈴。

正常和異常過程終止

下列事件序列會在正常程序終止期間發生。

針對裝置/上下文的每個 HWQueue:

  1. Dxgkrnl 呼叫 DxgkDdiDisconnectDoorbell 以中斷門鈴。
  2. Dxgkrnl 會等候最後一個佇列的 HwQueueProgressFenceLastQueuedValueCPUVirtualAddress 在 GPU 上完成。 環形緩衝區/環形緩衝區控制分配保持常駐。
  3. Dxgkrnl 的等候已滿足,它現在可以銷毀 Ring Buffer/Ring Buffer Control 配置項,以及門鈴和 HWQueue 對象。

在異常程序終止期間會發生下列事件序列。

  1. Dxgkrnl 將裝置標記為錯誤。

  2. 針對每個裝置內容, Dxgkrnl 會呼叫 DxgkddiSuspendContext 來暫停內容。 環緩衝區/環緩衝區控制配置仍常駐。 KMD 會佔用優先權,並將上下文從其硬體執行清單中移除。

  3. 針對每個上下文的 HWQueue,Dxglrnl

    一。 呼叫 DxgkDdiDisconnectDoorbell 來中斷門鈴連接。

    b。 終結 Ring Buffer/Ring Buffer Control 配置,以及門鈴和 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 上的 HWQueue 之間「虛擬化」和共用可用的門鈴。

KMD 的VictimizeDoorbell() 函數的偽代碼:

  • KMD 決定連接至PhysicalDoorbell1的邏輯門鈴hDoorbell1需要被犧牲並斷開連接。
  • 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. 作業系統會呼叫DxgkDdiConnectDoorbellhDoorbell2

    // 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 實體門鈴。