Windows顯示驅動(WDDM)編程初步(1)

歡迎轉載做者:張佩】【原文:http://www.yiiyee.cn/Blog/wddm1/web

WDDM Frame

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)。框架會在合適的時候調用這些回調函數,完成對應功能。

結構體定義以下:

InitializationData

 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事件。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做爲其中的一個輸入參數來使用。

WDDM Life

因此咱們看,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

相關文章
相關標籤/搜索