歡迎轉載【做者:張佩】【原文:http://www.yiiyee.cn/Blog/wddm1/】web
Windows顯示驅動從Vista開始,使用新的WDDM編程框架,稱爲Windows Display Driver Model。也有一種最初的名稱是LDDM,L表明Longhorn,但後來微軟在全部產品線上都再也不使用Longhorn代號,故而改爲如今的名稱。雖然在有些地方還能看到LDDM的說法,但應理解成舊文檔的遺存,不該該作概念上的區分。編程
WDDM框架是一種典型的小端口(miniport)驅動框架。NT系統中的全部小端口框架,都是基於WDM框架來實現的,但小端口框架對外提供了更高級的接口,以簡化編程的難度,並提升穩定性。以下圖所示,中間的WDDM是系統提供的編程框架,咱們基於這個框架,編寫裏面的小端口驅動,也就是顯示驅動。服務器
如今的顯卡設備,能夠按照功能將它分紅顯示和計算兩類。大部分的顯卡是用來鏈接顯示器顯示圖片和動畫用的,也有些顯卡主要確實用來作科學計算用的。顯卡處理器(GPU)對浮點運算有較強的能力,而主機處理器(CPU)處理浮點運算的能力較弱。而在科學計算領域,浮點運算是很是重要的內容,因此工業界就想到利用GPU進行科學運算。框架
應該說,全部的顯卡都既可以支持顯示,又可以支持運算。只是看它偏向哪一個方面,爲哪一個功能作優化罷了。對於偏重計算的顯卡,就沒必要配置多個顯示接口,圖像處理的模塊就不用很高級;相反,對於圖形功能偏重的顯卡,它就必需要大數據帶寬,大顯存,支持多種類型的接口,可以實現鋸齒優化等等。yii
針對咱們的驅動來說,若是一個顯示驅動,既支持顯卡的顯示功能,又支持運算功能,稱爲全功能驅動(Complete function);若是隻支持顯示,不支持運算,就是Display Only驅動;若是隻支持運算,不支持顯示功能,就是Render Only驅動。ide
微軟在Win8的系統上,爲全部不一樣類型的顯卡,編寫了Display Only和Render Only驅動。在未安裝廠商驅動或者廠商驅動被破壞、禁用的狀況下,系統會默認選擇使用Display Only驅動來顯示桌面內容。但通常系統不會選擇安裝Render Only驅動,那樣就什麼都看不到了。Render Only驅動的具體應用場景,我到目前尚未看到。可能在Render Only的數據服務器顯卡上會被運用。函數
個人這份顯示驅動初步教材,就是基於微軟公開的Display Only驅動項目KMDOD來寫的。不會涉及數據Render部分。其實能夠很方便地把一個Display only的驅動拓展到Complete驅動,在講完全部內容後,會有一小部份內容作介紹。大數據
KMDOD項目能夠從MSDN代碼網站上下載到,地址:http://code.msdn.microsoft.com/Kernel-mode-display-only-49adea58優化
若是不更改編譯配置,WDDM驅動的默認啓動函數是DriverEntry。這是驅動對象初始化的地方,通常對於小端口驅動而言,它須要調用框架的初始化函數。WDDM框架的初始化函數是DxgkInitializeDisplayOnlyDriver。從內核編程好幫手WDK中,能夠找到它的聲明:動畫
NTSTATUS DxgkInitializeDisplayOnlyDriver( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath, _In_ PKMDDOD_INITIALIZATION_DATA KmdDodInitializationData );
初始化函數會完成驅動對象的初始化,因此前面兩個參數是入口函數的輸入參數。在全文最後的實驗章節中,會介紹如何查看驅動對象,就可以比較清晰地看到WDDM框架對顯示驅動對象所進行的初始化做業了。此外,初始化函數還要完成顯示驅動相關的初始化。顯示驅動傳入一個函數結構體參數,類型是KMDDOD_INITIALIZATION_DATA,結構體裏面包含的是顯示驅動向框架提供的一系列回調函數(Callback Function)。框架會在合適的時候調用這些回調函數,完成對應功能。
結構體定義以下:
KMDOD項目沒有實現結構體中列舉的全部回調函數,因此它不能支持WDDM提供的所有和Display相關的功能。好比D3D用戶程序經過DC句柄和顯示驅動進行交互的escape回調函數,這裏就沒有實現。對於沒有實現的回調函數,在結構體中的對應函數指針應被初始化爲NULL。
第一個參數Version用來標識你所編寫的顯示驅動使用哪一個版本的WDDM。WDDM一共有四個版本:1.0(Vista & Vista SP1);1.1(Win7);1.2(Win8);1.3(Win Blue)。KMDOD這個項目中使用的是Win8版本:DXGKDDI_INTERFACE_VERSION_WIN8。
爲了完成結構體初始化,咱們要首先實現這些函數。在具體列舉實現代碼以前,把這些回調函數作一個簡單的分類和介紹是有必要的。
全部現代的物理設備都必須處理Pnp和Power事件。Pnp事件對應了設備插拔、開始、移除、中止等,以及做爲總線設備須要提供的子設備(顯示器)枚舉等;Power事件對應上電、掉電操做,以及查詢設備是否運行進行電源操做。
另外把卸載回調函數也納入其中。卸載函數在驅動被中止,沒有任何外部模塊引用的時候,系統會嘗試將驅動卸載,這時候卸載回調被調用。
InitialData.DxgkDdiAddDevice = BddDdiAddDevice; InitialData.DxgkDdiStartDevice = BddDdiStartDevice; InitialData.DxgkDdiStopDevice = BddDdiStopDevice; InitialData.DxgkDdiStopDeviceAndReleasePostDisplayOwnership = BddDdiStopDeviceAndReleasePostDisplayOwnership; InitialData.DxgkDdiResetDevice = BddDdiResetDevice; InitialData.DxgkDdiRemoveDevice = BddDdiRemoveDevice; InitialData.DxgkDdiQueryChildRelations = BddDdiQueryChildRelations; InitialData.DxgkDdiQueryChildStatus = BddDdiQueryChildStatus; InitialData.DxgkDdiQueryDeviceDescriptor = BddDdiQueryDeviceDescriptor; InitialData.DxgkDdiSetPowerState = BddDdiSetPowerState; InitialData.DxgkDdiUnload = BddDdiUnload; InitialData.DxgkDdiQueryAdapterInfo = BddDdiQueryAdapterInfo;
顯卡驅動的主要功能是配置物理設備,讓它可以輸出圖片和動畫到外部顯示設備上。和這個功能相關的函數有不少,它包括對鼠標位置的更新,顯示器Mode的枚舉和設置等函數:
InitialData.DxgkDdiSetPointerPosition = BddDdiSetPointerPosition; InitialData.DxgkDdiSetPointerShape = BddDdiSetPointerShape; InitialData.DxgkDdiIsSupportedVidPn = BddDdiIsSupportedVidPn; InitialData.DxgkDdiRecommendFunctionalVidPn = BddDdiRecommendFunctionalVidPn; InitialData.DxgkDdiEnumVidPnCofuncModality = BddDdiEnumVidPnCofuncModality; InitialData.DxgkDdiSetVidPnSourceVisibility = BddDdiSetVidPnSourceVisibility; InitialData.DxgkDdiCommitVidPn = BddDdiCommitVidPn; InitialData.DxgkDdiUpdateActiveVidPnPresentPath = BddDdiUpdateActiveVidPnPresentPath; InitialData.DxgkDdiRecommendMonitorModes = BddDdiRecommendMonitorModes;
最後是和物理設備交互的一些函數,首先是中斷處理函數,而後有獲取設備屬性,讀寫設備幀內存、顯示桌面內容(Present)等函數。
InitialData.DxgkDdiDpcRoutine = BddDdiDpcRoutine; InitialData.DxgkDdiInterruptRoutine = BddDdiInterruptRoutine; InitialData.DxgkDdiQueryVidPnHWCapability = BddDdiQueryVidPnHWCapability; InitialData.DxgkDdiPresentDisplayOnly = BddDdiPresentDisplayOnly; InitialData.DxgkDdiSystemDisplayEnable = BddDdiSystemDisplayEnable; InitialData.DxgkDdiSystemDisplayWrite = BddDdiSystemDisplayWrite;
這部分是顯示驅動做爲一個驅動來說,它所實現的通常意義上的功能支持函數。這部分我只列了一個,是用戶程序和內核驅動交互用的IO控制函數。
InitialData.DxgkDdiDispatchIoRequest = BddDdiDispatchIoRequest;
完整的初始化函數:
extern "C" NTSTATUS DriverEntry( _In_ DRIVER_OBJECT* pDriverObject, _In_ UNICODE_STRING* pRegistryPath) { PAGED_CODE(); // Initialize DDI function pointers and dxgkrnl KMDDOD_INITIALIZATION_DATA InitialData = {0}; InitialData.Version = DXGKDDI_INTERFACE_VERSION_WIN8; InitialData.DxgkDdiAddDevice = BddDdiAddDevice; //…… 其它的回調函數賦值過程,上面已所有列舉,此處省略 NTSTATUS Status = DxgkInitializeDisplayOnlyDriver(pDriverObject, pRegistryPath, &InitialData); if (!NT_SUCCESS(Status)) { BDD_LOG_ERROR1("DxgkInitializeDisplayOnlyDriver failed with Status: 0x%I64x", Status); } return Status; }
初始化部分到此原本能夠結束,繼續講下面的回調函數實現。但其實依然能夠擴展一下,不熟悉WDM的讀者能夠跳過。針對全部的端口驅動框架都基於WDM來實現的這個事實,若是有的小端口驅動有必要想直接操做IRP的話,應該怎麼實現呢?
其實很是簡單。在DxgkInitializeDisplayOnlyDriver被調用過以後,驅動對象的初始化已經完成了。這時候咱們能夠對框架的分發函數進行Hook。
好比有一個很重要的功能,不少設備驅動都要作的。就是它但願本身可以獲得系統關機的訊息。經過通常意義上的PNP和Power事件,是沒有辦法獲得系統關機訊息的。辦法是註冊本身的IRP_MJ_SHUTDOWN分發函數來接收此訊息。
下面是簡要的實現代碼:
// 下面代碼應在DxgkInitializeDisplayOnlyDriver被調用事後執行 pOldFunc = pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN]; // 保存框架有可能已實現的函數 pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = BddDdiShutdown; // 此外還須要在StartDevice函數中調用IoRegisterShutdownNotificatin函數,本文再也不繼續演示 NTSTATUS BddDdiShutdown (PDEVICE_OBJECT pDev, PIRP irp) { // do something you wanted here if (pOldFunc) return pOldFunc (pDev, irp); else return STATUS_SUCCESS; }
KMDOD項目定義了一個顯示驅動類,來封裝和實際功能相關的全部具體操做。這樣一來,大部分回調函數的實現都比較簡單。這個類是BASIC_DISPLAY_DRIVER,咱們在第二節會具體地講它。
WDDM框架在設計的時候,是可以支持多個底層物理設備的。換句話說,若是系統中存在多個顯卡設備,WDDM框架都可以很好地支持它們同時或者分別工做。做爲這個支持的一部分,在PNP操做的起始,也就是AddDevice回調函數被調用的時候,框架要求顯示驅動返回一個當前物理設備的Context,做爲識別此物理設備的標識。
那麼KMDOD也是能夠支持多個顯卡設備的,因此爲每一個設備建立一個BASIC_DISPLAY_DRIVER對象,做爲Context返回給框架。框架在之後調用任何一個回調函數時,都會把這個Context做爲其中的一個輸入參數來使用。
因此咱們看,DriverEntry是驅動的開始,卸載函數是驅動的結束;AddDevice函數是設備工做的開始,RemoveDevice是設備結束工做的標識。咱們能夠用下面的框圖來描述這個概念。
設備的Context做爲一個標識物理顯卡的變量,在設備的PNP週期裏面一直運做着。當關機、設備禁用或者其餘變故發生的時候,RemoveDevice回調被執行,顯示驅動將負責刪除它所建立的設備Context。
下面是AddDevice和RemoveDevice這兩個回調函數的實現。
NTSTATUS BddDdiAddDevice( _In_ DEVICE_OBJECT* pPhysicalDeviceObject, _Outptr_ PVOID* ppDeviceContext) { PAGED_CODE(); if ((pPhysicalDeviceObject == NULL) || (ppDeviceContext == NULL)) { BDD_LOG_ERROR2("One of pPhysicalDeviceObject (0x%I64x), ppDeviceContext (0x%I64x) is NULL", pPhysicalDeviceObject, ppDeviceContext); return STATUS_INVALID_PARAMETER; } *ppDeviceContext = NULL; BASIC_DISPLAY_DRIVER* pBDD = new(NonPagedPoolNx) BASIC_DISPLAY_DRIVER(pPhysicalDeviceObject); if (pBDD == NULL) { BDD_LOG_LOW_RESOURCE0("pBDD failed to be allocated"); return STATUS_NO_MEMORY; } *ppDeviceContext = pBDD; return STATUS_SUCCESS; } NTSTATUS BddDdiRemoveDevice( _In_ VOID* pDeviceContext) { PAGED_CODE(); BASIC_DISPLAY_DRIVER* pBDD = reinterpret_cast<BASIC_DISPLAY_DRIVER*>(pDeviceContext); if (pBDD) { delete pBDD; pBDD = NULL; } return STATUS_SUCCESS; }
其它回調函數的實現,這裏僅僅以StartDevice爲例講解。函數原型定義以下:
NTSTATUS DxgkDdiStartDevice( _In_ const PVOID MiniportDeviceContext, _In_ PDXGK_START_INFO DxgkStartInfo, _In_ PDXGKRNL_INTERFACE DxgkInterface, _Out_ PULONG NumberOfVideoPresentSources, _Out_ PULONG NumberOfChildren )
第一個參數即設備Context,毫無疑問,它就是咱們剛剛在AddDevice中建立的BASIC_DISPLAY_DRIVER對象。因此咱們第一步須要獲取對象指針,而且調用到BASIC_DISPLAY_DRIVER裏面的startDevice函數中去。其實現以下:
NTSTATUS BddDdiStartDevice( _In_ VOID* pDeviceContext, _In_ DXGK_START_INFO* pDxgkStartInfo, _In_ DXGKRNL_INTERFACE* pDxgkInterface, _Out_ ULONG* pNumberOfViews, _Out_ ULONG* pNumberOfChildren) { PAGED_CODE(); BDD_ASSERT_CHK(pDeviceContext != NULL); BASIC_DISPLAY_DRIVER* pBDD = reinterpret_cast<BASIC_DISPLAY_DRIVER*>(pDeviceContext); return pBDD->StartDevice(pDxgkStartInfo, pDxgkInterface, pNumberOfViews, pNumberOfChildren); }
其它的回調函數,實現方法和StartDevice很相似。惟一的區別是,若是StartDevice調用失敗的話,也就是設備啓動失敗的話,道理上講,不少後續的函數都不該該被調用,由於既然設備沒有啓動,就不該該有任何針對於它的動做存在。因此這些函數被調用的時候,不少都會先確認一下,設備是否處於啓動狀態(IsDriverActive),就是判斷StartDevice是否執行成功。
V1.0:2013/7/23