欢迎转载【作者:张佩】【原文:http://www.yiiyee.cn/Blog/wddm1/】
Windows显示驱动从Vista开始,使用新的WDDM编程框架,称为Windows Display Driver Model。也有一种最初的名称是LDDM,L代表Longhorn,但后来微软在所有产品线上都不再使用Longhorn代号,故而改成现在的名称。虽然在有些地方还能看到LDDM的说法,但应理解成旧文档的遗存,不应该做概念上的区分。
WDDM框架是一种典型的小端口(miniport)驱动框架。NT系统中的所有小端口框架,都是基于WDM框架来实现的,但小端口框架对外提供了更高级的接口,以简化编程的难度,并提高稳定性。如下图所示,中间的WDDM是系统提供的编程框架,我们基于这个框架,编写里面的小端口驱动,也就是显示驱动。
显示驱动类型
现在的显卡设备,可以按照功能将它分成显示和计算两类。大部分的显卡是用来连接显示器显示图片和动画用的,也有些显卡主要确实用来做科学计算用的。显卡处理器(GPU)对浮点运算有较强的能力,而主机处理器(CPU)处理浮点运算的能力较弱。而在科学计算领域,浮点运算是非常重要的内容,所以工业界就想到利用GPU进行科学运算。
应该说,所有的显卡都既能够支持显示,又能够支持运算。只是看它偏向哪个方面,为哪个功能做优化罢了。对于偏重计算的显卡,就不必配置多个显示接口,图像处理的模块就不用很高级;相反,对于图形功能偏重的显卡,它就必须要大数据带宽,大显存,支持多种类型的接口,能够实现锯齿优化等等。
针对我们的驱动来讲,如果一个显示驱动,既支持显卡的显示功能,又支持运算功能,称为全功能驱动(Complete function);如果只支持显示,不支持运算,就是Display Only驱动;如果只支持运算,不支持显示功能,就是Render Only驱动。
微软在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事件。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
30,241 total views, 8 views today
加油,作者!很有用!
你好!请教一个问题:我想把KMDOD的sample改造一下,使这个driver挂一个虚拟的display,这样OS以为有两个monitor(包括一个硬件monitor),请问这应该怎么做呢?非常感谢!
我没有做过这个。但我想,报子设备的时候,多报一个虚拟设备就可以了。你要在BddDdiQueryDeviceDescriptor函数中处理,在这个函数里面,你需要手动配置一个EDID。有进一步的情况,请让我知道。
按你说的,真的成功了!非常感谢!
能详细说明一下您怎么做的吗?谢谢
看了一下,和AVStream/BDA TV tuner Miniport Driver类似的开发过程
小端口驱动,或多或少,都有点类似。
楼主你好,最近想了解下显示方面的驱动。搜索来到这个帖子,对这方面有一点疑惑。
对于不是显卡厂家的开发人员来说,这种驱动有什么意义呢? 这些介绍是不是针对显卡厂家的显卡驱动开发人员来做的,别人的会可以用这方面的东西做出什么类型的应用来? dxgkrnl.sys文件在WDDM中和其他的显卡驱动的关系是怎么样的? dxgkrnl是不是最后会调用显卡厂家提供的显卡驱动? 显卡厂家提供的显卡驱动必须遵循WDDM的框架规则才能让dxgkrnl.sys来调用?
期待你的回复。
你好,回答如下:
>>对于不是显卡厂家的开发人员来说,这种驱动有什么意义呢?
每种显卡厂商的设备,都有自己的硬件设计,显卡的硬件设计是没有统一的硬件标准的(除了和系统的接口部分),所以总是由厂商提供显卡驱动。系统的display only驱动,或者本文所依赖的KMDOD驱动,都没有利用硬件加速,仅仅使用显卡进行显示。显示功能是使用了通用的接口,就是GPU总是把它Frame Buffer(简称FB)映射到系统的物理地址空间中,系统把要显示的内容Present到FB中。
>>这些介绍是不是针对显卡厂家的显卡驱动开发人员来做的,别人的会可以用这方面的东西做出什么类型的应用来?
有一些应用的,但不是很多。可以用来扩展桌面,我记得iDisplay这个软件,会安装一个WDDM驱动。
>>dxgkrnl.sys文件在WDDM中和其他的显卡驱动的关系是怎么样的? dxgkrnl是不是最后会调用显卡厂家提供的显卡驱动? 显卡厂家提供的显卡驱动必须遵循WDDM的框架规则才能让dxgkrnl.sys来调用?
dxgkrnl就是文章中讲的WDDM框架。它和小端口驱动的关系,文章中这幅图进行了描述,二者互有接口进行调用。
I like to party, not look artielcs up online. You made it happen.
关于硬件加速跟3D交互显示的部分是怎么样的? 按你所说,diaplay_only是把 FB中的内送送出到显示器,那硬件加速跟3D交互显示是形成 FB中的内容的过程?
硬件加速就是3D运算也由GPU实现。现在的GPU还可以实现部分通用计算,比如提出的GPGPU概念。
这个驱动sample我在虚拟机的串口win8上debug的时候,总是会出现黄色感叹号,显示:由于 Windows 无法加载这个设备所需的驱动程序,导致这个设备工作异常。
Thanks for sharing. Your post is a useful corbtinution.
Why had you just explain very basic’s and pasted all the code here.it is not fair.can you elaborate more about usermode dll for display if yes mail me at mhere4smthng@gmail.com
您好,张佩
我有个需求,但没有显卡驱动经验,望给予指点:
为的笔记本显卡是NVIDIA Quadro FX 2800M,这块显卡在win7和win8下的驱动有问题,就是会随时崩溃,黑屏,使用微软的base display adapter可以稳定使用,但不能接第二块屏,码农需要第二块屏,有什么方法可以改造显示驱动程序,能用起来第二块屏?linux下的Nouveau可以满足要求,但linux的Nvidia驱动同样容易崩溃。能否通过配置文件就实现为的需求?(配置base display adapter使之能支持双屏,或配置base display adapter使之不再崩溃)。
另外,崩溃的原因可能是使用的显卡的高级特性,我采用NVIDIA windows下的早期驱动,再降低工作频率稳定性能勉强满足要求。
您好.老师,请问我怎么找不到那个 sample了,能够发给我一份么?我想做一个虚拟的显卡驱动,可以支持超高的分辨率显示…谢谢了
有进展吗
Which came first, the problem or the sotuilon? Luckily it doesn’t matter.
你好,请问下显示器mode的枚举具体是哪个函数,以及mode的设置是哪个函数,谢谢啊。
http://code.msdn.microsoft.com/Kernel-mode-display-only-49adea58 这个地址失效了,显示“此项尚未发布。”,微软也太扯淡了吧。
你好,请问我在BddDdiPresentDisplayOnly函数中能获取一些图片,该图片由应用程序发送的,我想知道发送该图片的应用程序进程ID,我该如何获取,在xddm框架中我通过EngGetCurrentProcessId就可以了,我在WDDM中使用psGetCurrentProcessId获取,但是返回值一直是4,表示的是system.exe这个进程,请问我该怎么做呢?谢谢!
你好,我想请教一下关于hook的问题。
其实非常简单。在DxgkInitializeDisplayOnlyDriver被调用过之后,驱动对象的初始化已经完成了。这时候我们可以对框架的分发函数进行Hook。
nvidia的小端驱动应该是nvlddmkm.sys,如何获得分发函数的地址了?
比如一个游戏程序,想hook DxgkDdiCreateAllocation,游戏本身加载一个nt驱动,能在nt驱动里得到DxgkDdiCreateAllocation地址了?
谢谢。
你好!请教一个问题:我怎么获取和修改显存中的内容。
https://github.com/Microsoft/Windows-driver-samples/tree/master/video/KMDOD