使用者模式工作提交
重要
某些資訊與發行前版本產品有關,在發行前版本產品可能經過大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。
本文說明從 Windows 11 版本 24H2(WDDM 3.2)起仍在開發的使用者模式 (UM) 工作提交功能。 UM 工作提交可讓應用程式直接從使用者模式將工作提交到 GPU,且延遲非常低。 目標是改善經常將小型工作負載提交至 GPU 的應用程式效能。 此外,如果使用者在容器或虛擬機內執行,使用者模式提交將會大幅受益。 此優點是因為在 VM 中執行的使用者模式驅動程式 (UMD) 可以直接將工作提交至 GPU,而不需要將訊息傳送至主機。
支援UM工作提交的IHV驅動程式和硬體必須繼續同時支援傳統的內核模式工作提交模型。 對於較舊的客體,僅支援在最新主機上執行的傳統 KM 佇列,此支援是必要的。
本文不會討論 UM 提交與 Flip/FlipEx 的互操作性。 本文所述的 UM 提交僅限於轉譯/計算案例類別。 簡報管線目前會繼續以核心模式提交為基礎,因為它與原生受監視的柵欄相依性。 一旦原生監視的柵欄和僅限計算/轉譯的 UM 提交完整實作,就可以考慮 UM 提交型簡報的設計和實作。 因此,驅動程式應該支援每個佇列的使用者模式提交。
門鈴
支持硬體排程的大多數最新或即將推出的 GPU 也支援 GPU 門鈴的概念。 門鈴是向 GPU 引擎指出新工作在其工作佇列中排入佇列的機制。 Doorbell 通常會在PCIe BAR(基位址列)或系統記憶體中註冊。 每個 GPU IHV 都有自己的架構,可決定門鈴的數目、它們位於系統中的位置等等。 Windows OS 會使用門鈴做為其設計的一部分,以實作 UM 工作提交。
概括而言,有兩種不同的門鈴模型是由不同的 IHV 和 GPU 實作:
全域門鈴
在 Global Doorbells 模型中,所有內容和處理程式的硬體佇列都會共用單一全域門鈴。 寫入門鈴的值會通知 GPU 排程器哪些特定硬體佇列和引擎有新的工作。 如果多個硬體佇列主動提交工作並響鈴,GPU 硬體會使用輪詢機制的形式來擷取工作。
專用門鈴
在專用門鈴模型中,每個硬體佇列都會指派自己的門鈴,每當有新的工作要提交至 GPU 時,就會執行。 當門鈴執行時,GPU 排程器會確切知道哪些硬體佇列送出新工作。 在 GPU 上建立的所有硬體佇列中共用有限的門鈴。 如果建立的硬體佇列數目超過可用的門鈴數目,司機必須中斷舊或最近使用硬體佇列的門鈴的聯機,並將其門鈴指派給新建立的佇列,有效地“虛擬化”門鈴。
探索使用者模式工作提交支援
DXGK_NODEMETADATA_FLAGS::UserModeSubmissionSupported
對於支援 UM 工作提交功能的 GPU 節點,KMD 的 DxgkDdiGetNodeMetadata 會設定新增至DXGK_NODEMETADATA_FLAGS的 UserModeSubmissionSupported 節點元數據旗標。 然後OS允許UMD只在設定此旗標的節點上建立使用者模式提交HWQueues和門鈴。
DXGK_QUERYADAPTERINFOTYPE::D XGKQAITYPE_USERMODESUBMISSION_CAPS
若要查詢門鈴特定資訊,OS 會使用DXGKQAITYPE_USERMODESUBMISSION_CAPS查詢配接器資訊類型呼叫 KMD 的 DxgkDdiQueryAdapterInfo 函式。 KMD 會藉由填入 DXGK_USERMODESUBMISSION_CAPS 結構及其使用者模式工作提交的支持詳細數據來回應。
目前,唯一需要的上限是門鈴記憶體大小(以位元組為單位)。 Dxgkrnl 需要門鈴記憶體大小,原因如下:
- 在門鈴建立期間(D3DKMTCreateDoorbell),Dxgkrnl 會將 DoorbellCpuVirtualAddress 傳回 UMD。 這麼做之前, Dxgkrnl 必須先在內部對應至虛擬頁面,因為門鈴尚未指派並連線。 配置虛擬頁面需要門鈴的大小。
- 在門鈴連接期間(D3DKMT 連線 Doorbell),Dxgkrnl 需要將 DoorbellCpuVirtualAddress 旋轉至 KMD 提供的 DoorbellPhysicalAddress。 同樣地, Dxgkrnl 需要知道門鈴大小。
D3DDDI_CREATEHWQUEUEFLAGS::D 3DKMTCreateHwQueue 中的 UserModeSubmission
UMD 會設定新增至D3DDDI_CREATEHWQUEUEFLAGS的 UserModeSubmission 旗標,以建立使用使用者模式提交模型的 HWQueues。 使用此旗標建立的 HWQueues 無法使用一般內核模式工作提交路徑,而且必須依賴佇列上工作提交的門鈴機制。
使用者模式工作提交 API
新增下列使用者模式 API 以支援使用者模式工作提交。
D3DKMT 連線 Doorbell 會將先前建立的門鈴連線到 D3D HWQueue,以進行使用者模式工作提交。
D3DKMTNotifyWorkSubmission 會通知 KMD 在 HWQueue 上提交新工作。 這項功能的重點是低延遲的工作提交路徑,其中 KMD 未涉及或知道何時提交工作。 此 API 適用於在 HWQueue 上提交工作時,必須通知 KMD 的情況。 驅動程式應該在特定且不頻繁的情況下使用此機制,因為它牽涉到每個工作提交的 UMD 到 KMD 的來回行程,因此會挫敗低延遲使用者模式提交模型的用途。
門鈴記憶體和環形緩衝區配置的落地模型
- UMD 負責在建立門鈴之前,讓環形緩衝區和環形緩衝區控制配置常駐。
- UMD 會管理通道緩衝區和通道緩衝區控制配置的存留期。 Dxgkrnl 不會隱含地終結這些配置,即使對應的門鈴被終結也一樣。 UMD 負責配置和終結這些配置。 不過,為了防止惡意的使用者模式程式在門鈴運作時終結這些配置, Dxgkrnl 會在門鈴存留期間參考這些配置。
- Dxgkrnl 終結通道緩衝區配置的唯一案例是在裝置終止期間。 Dxgkrnl 會終結與裝置相關聯的所有 HWQueues、門鈴和環形緩衝區配置。
- 只要通道緩衝區配置運作,通道緩衝區 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可以使用暫停的內容呼叫 D3DKMTSubmitCommand。 Dxgkrnl 會將這個新命令加入 HwQueue,但直到稍後才會排程。
下列事件順序會在硬體內容暫停和繼續期間發生。
暫停硬體內容:
- Dxgkrnl 會呼叫 DxgkddiSuspendContext。
- KMD 會從 HW 排程器清單中移除內容的所有 HWQueue。
- 門鈴仍然連接,環形緩衝區/環形緩衝區控制配置仍為居民。 UMD 可以將新的命令寫入此內容的 HWQueue,但 GPU 不會處理它們,這類似於現今的核心模式命令提交至暫停的內容。
- 如果 KMD 選擇犧牲暫停 HWQueue 的門鈴,則 UMD 會失去其連接。 UMD 可以嘗試重新連線門鈴,KMD 會將新的門鈴指派給此佇列。 其意圖不是停止 UMD,而是允許它繼續提交 HW 引擎在內容恢復后最終可以處理的工作。
繼續硬體內容:
- Dxgkrnl 會呼叫 DxgkddiResumeContext。
- KMD 會將內容的所有 HWQueue 新增至 HW 排程器的清單。
引擎 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 時呼叫 D3DKMT 連線 Doorbell,則會強制 Dxgkrnl 將 GPU 喚醒為 D0。 Dxgkrnl 也負責繼續 HWQueue 並將門鈴 CPUVA 旋轉至實體門鈴位置。
下列事件順序會進行。
發生 D0 到 D3 GPU 電源關閉:
- Dxgkrnl 會針對 GPU 上的所有 HW 內容呼叫 DxgkddiSuspendContext 。 KMD 會從 HW 排程器清單中移除這些內容。
- Dxgkrnl 會中斷所有門鈴的連線。
- Dxgkrnl 可能會在必要時從 VRAM 收回所有 Ring Buffer/Ring Buffer Control 配置。 一旦所有內容都暫停並從硬體排程器清單中移除,硬體就不會參考任何收回的記憶體,就會執行此動作。
UMD 會在 GPU 處於 D3 狀態時,將新的命令寫入 HWQueue:
- UMD 看到門鈴已中斷連線,因此呼叫 D3DKMT 連線 Doorbell。
- Dxgkrnl 會起始 D0 轉換。
- Dxgkrnl 會讓所有 Ring Buffer/Ring Buffer Control 配置都位於已收回時。
- Dxgkrnl 會呼叫 KMD 的 DxgkddiCreateDoorbell 函式,要求 KMD 為此 HWQueue 建立門鈴 連線。
- Dxgkrnl 會呼叫 所有 HWContexts 的 DxgkddiResumeContext 。 KMD 會將對應的佇列新增至 HW 排程器清單。
使用者模式工作提交的 DIS
KMD 實作的 DIS
KMD 會新增下列核心模式 DIS,以實作使用者模式工作提交支援。
DxgkDdiCreateDoorbell。 當 UMD 呼叫 D3DKMTCreateDoorbell 來建立 HWQueue 的門鈴時,Dxgkrnl 對此函式發出對應的呼叫,讓 KMD 可以初始化其門鈴結構。
DxgkDdi 連線 Doorbell。 當UMD呼叫 D3DKMT 連線 Doorbell 時,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 會在命令緩衝區完成時產生和寫入。 為了讓 Dxgkrnl 知道特定硬體佇列是否有擱置中工作,UMD 必須在將新的命令緩衝區附加至通道緩衝區並讓 GPU 看到之前,先更新伩列進度柵欄值。 CreateDoorbell.HwQueueProgressFenceLastQueuedValueCPUVirtualAddress 是最新佇列值的讀取/寫入使用者模式進程對應。
UMD 在 GPU 看到新的提交之前,務必先更新佇列值。 下列步驟是建議的作業順序。 他們假設 HW 佇列處於閑置狀態,而最後一個完成的緩衝區具有 N 的進度柵欄值。
- 產生新的進度柵欄值 N+1。
- 填寫命令緩衝區。 命令緩衝區的最後一個指令是寫入 N+1 的進度柵欄值。
- 將 *(HwQueueProgressFenceLastQueuedValueCPUVirtualAddress) 設定為 N+1,以通知 OS 新佇列的值。
- 將命令緩衝區新增至通道緩衝區,讓 GPU 可見。
- 敲門鈴。
正常和異常的進程終止
下列事件順序會在一般進程終止期間發生。
針對裝置/內容的每個 HWQueue:
- Dxgkrnl 會呼叫 DxgkDdiDisconnectDoorbell 來中斷門鈴 的連線。
- Dxgkrnl 會等候最後一個佇列 HwQueueProgressFenceLastQueuedValueCPUVirtualAddress 在 GPU 上完成。 信號緩衝區/信號緩衝區控制配置仍會保持常駐狀態。
- Dxgkrnl 的等候已滿足,而且現在可以終結 Ring Buffer/Ring Buffer Control 配置,以及 Doorbell 和 HWQueue 物件。
下列事件順序會在異常進程終止期間發生。
Dxgkrnl 會將裝置標示為錯誤。
針對每個裝置內容, Dxgkrnl 會呼叫 DxgkddiSuspendContext 來暫停內容。 Ring Buffer/Ring Buffer Control 配置仍為常駐。 KMD 會先占內容,並從其 HW 執行清單中移除它。
針對內容的每個 HWQueue,Dxglrnl:
a. 呼叫 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 上的 HWQueues 之間共用可用的門鈴。
KMD 函式 VictimizeDoorbell()
的虛擬程式碼:
- KMD 決定連線到
PhysicalDoorbell1
的邏輯門鈴hDoorbell1
需要犧牲和中斷連線。 - KMD 會呼叫 Dxgkrnl 的
DxgkCbDisconnectDoorbellCB(hDoorbell1->hHwQueue)
。- Dxgkrnl 會將此門鈴的 UMD 可見 CPUVA 旋轉至虛擬頁面,並將狀態值更新為D3DDDI_DOORBELL_STATUS_DISCONNECTED_RETRY。
- KMD 會恢復控制,並執行實際的犧牲/中斷連線。
- KMD 會犧牲
hDoorbell1
,並將它與PhysicalDoorbell1
中斷連線。 PhysicalDoorbell1
可供使用
- KMD 會犧牲
現在,請考慮下列案例:
PCI BAR 中有單一實體門鈴,核心模式 CPUVA 等於
0xfeedfeee
。 為 HWQueue 建立的 Doorbell 物件會指派此實體門鈴值。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
OS 會呼叫
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
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 使用全域門鈴,則不需要此機制。 相反地,在此範例中, hDoorbell1
和 hDoorbell2
都會被指派相同的 0xfeedfeee
實體門鈴。