欢迎转载【做者:张佩】【原文: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