驱动程序开发第六部分:显示驱动程序入门






4.94/5 (82投票s)
2006年1月30日
20分钟阅读

646340

8311
Windows显示驱动程序模型简介。
引言
这个系列的文章更新了一段时间了,我找到一些空闲时间来编写下一部分。在本文中,我们将探讨如何编写一个简单的显示驱动程序。显示驱动程序是一种特殊的驱动程序,它属于一个与我们在此系列中迄今为止讨论过的框架不同的框架。
本文的示例驱动程序将展示如何编写一个不关联任何硬件的基本显示驱动程序。相反,此显示驱动程序将实现图形到内存,并将使用一个应用程序来显示这些图形。我曾在为C/C++ User's Journal撰写的一篇文章中演示过此方法,但那篇文章是关于扩展 VMWare 以支持多显示器。本文将仅关注显示驱动程序本身,并且不使用 VMWare,而仅需要您的本地计算机。
显示驱动程序架构
首先,我们来展示 Windows NT 中的显示驱动程序架构。我需要说明一点,Windows Vista 引入了一种新的显示驱动程序模型,称为 LDDM。这对支持新的桌面窗口管理器至关重要,但 Windows Vista 仍然支持旧的显示驱动程序模型以及旧的窗口管理器。本文将不介绍 LDDM。
显示驱动程序模型由两部分组成:微型端口驱动程序和显示驱动程序。微型端口驱动程序加载到系统空间,负责枚举设备和管理设备资源。显示驱动程序加载到会话空间,负责实现实际的 GDI 图形调用。驱动程序负责实现这些调用,具体实现方式由其自行决定,可以通过软件实现,也可以将其推迟到图形卡本身。显示驱动程序完全控制线条如何绘制或透明效果如何实现。
下图显示了 Windows 显示驱动程序架构
显示微型端口
微型端口驱动程序加载到系统空间,负责管理显示设备资源和枚举设备。但是,此驱动程序使用另一个驱动程序作为其框架,即 *VIDEOPRT.SYS*。此驱动程序导出您驱动程序将链接并使用的 API。您是否对驱动程序可以导出 API 感到惊讶?不必。驱动程序使用 PE 格式,并具有导出和导入表。您可以从驱动程序导出 API,并允许其他驱动程序像 DLL 一样链接并使用它们。事实上,您使用的所有 API 都只是链接到内核和其他驱动程序。
我需要指出,链接到内核和用户模式驱动程序之间存在细微差别。如果一个驱动程序链接到一个当前未加载到内存中的驱动程序,则该驱动程序将被加载到内存中,但该驱动程序的 `DriverEntry` 不会被调用。`DriverEntry` 本身直到驱动程序通过 `ZwLoadDriver` 直接加载,由系统加载,或者如我们之前所示通过服务 API 加载才会调用。在任何情况下,您都可以从一个驱动程序导出 API,并从另一个驱动程序链接并使用这些 API。内核中没有“GetProcAddress” API,因此您需要自己编写一个。
无论如何,*VideoPrt.SYS* 导出了您微型端口驱动程序将调用的 API。此驱动程序执行几项任务,其中一项是实现通用代码,以便视频驱动程序编写者无需重写相同的代码。这些代码包括 WIN32 子系统 (*WIN32K.SYS*) 和您的微型端口之间的视频设备枚举。*VideoPrt.SYS* 还会创建显示器的设备对象,当您调用初始化例程时,它会将您的驱动程序对象的入口点链接到 *VideoPrt.SYS*!
*VideoPrt.SYS* API 都以“VideoPort”开头,您调用的第一个 API 是“VideoPortInitialize
”。如果您注意到前两个参数是传递给您的 `DriverEntry` 例程的参数,但它只是将它们称为“Context1”和“Context2”,就好像您的视频微型端口驱动程序是“特殊的”一样。不要被愚弄,这个驱动程序入口与我们之前处理的相同,第一个“Context1”实际上是您的驱动程序对象。一旦您将驱动程序对象传递给 `VideoPortInitialize`,您驱动程序的所有入口点都将被链接到 *VideoPrt.Sys*。相反,您在“VIDEO_HW_INITIALIZATION_DATA
”中传递不同的函数指针,当需要时 *VideoPrt.SYS* 将会调用它们。
这意味着您无需直接处理视频微型端口中的 IRP。*VideoPrt.SYS* 将会处理它们,分解它们,然后确定何时需要通知您有关数据的信息。相反,您需要处理他们称之为“VRP”或“Video Request Packet”的东西。这本质上是 IRP 的一个简化版本,采用不同的数据结构。您只需要返回即可,无需像处理 IRP 那样进行特殊处理。
文档规定,您应该只在微型端口中使用“VideoPort” API,但由于这只是一个常规的系统级驱动程序,您仍然可以链接您希望使用的任何内核 API,我以前这样做过。显示驱动程序本身的情况并非如此,我们稍后会看到。
由于我们没有任何硬件,我们的微型端口驱动程序将非常精简且易于编写。以下代码显示了视频微型端口 `DriverEntry` 的构造方式
/********************************************************************** * * DriverEntry * * This is the entry point for this video miniport driver * **********************************************************************/ ULONG DriverEntry(PVOID pContext1, PVOID pContext2) { VIDEO_HW_INITIALIZATION_DATA hwInitData; VP_STATUS vpStatus; /* * The Video Miniport is "technically" restricted to calling * "Video*" APIs. * There is a driver that encapsulates this driver by setting your * driver's entry points to locations in itself. It will then * handle your IRP's for you and determine which of the entry * points (provided below) into your driver that should be called. * This driver however does run in the context of system memory * unlike the GDI component. */ VideoPortZeroMemory(&hwInitData, sizeof(VIDEO_HW_INITIALIZATION_DATA)); hwInitData.HwInitDataSize = sizeof(VIDEO_HW_INITIALIZATION_DATA); hwInitData.HwFindAdapter = FakeGfxCard_FindAdapter; hwInitData.HwInitialize = FakeGfxCard_Initialize; hwInitData.HwStartIO = FakeGfxCard_StartIO; hwInitData.HwResetHw = FakeGfxCard_ResetHW; hwInitData.HwInterrupt = FakeGfxCard_VidInterrupt; hwInitData.HwGetPowerState = FakeGfxCard_GetPowerState; hwInitData.HwSetPowerState = FakeGfxCard_SetPowerState; hwInitData.HwGetVideoChildDescriptor = FakeGfxCard_GetChildDescriptor; vpStatus = VideoPortInitialize(pContext1, pContext2, &hwInitData, NULL); return vpStatus; }
我之前提到过,您只需将 `DriverObject` 直接传递给 *VideoPrt.SYS* 驱动程序,如上所示。您还可以填写一个数据结构,其中包含您驱动程序中的入口点,*VideoPrt.SYS* 驱动程序将调用这些入口点来执行各种操作。“`HwStartIO`”是您处理 IOCTL 的地方,您可以在显示驱动程序和视频微型端口之间使用 IOCTL。显示驱动程序只需调用“EngDeviceIoControl
”,这个 IOCTL 将在微型端口的 `HwStartIO` 中处理。
以下显示了我如何实现视频微型端口函数
/*#pragma alloc_text(PAGE, FakeGfxCard_ResetHW) Cannot be Paged*/ /*#pragma alloc_text(PAGE, FakeGfxCard_VidInterrupt) Cannot be Paged*/ #pragma alloc_text(PAGE, FakeGfxCard_GetPowerState) #pragma alloc_text(PAGE, FakeGfxCard_SetPowerState) #pragma alloc_text(PAGE, FakeGfxCard_GetChildDescriptor) #pragma alloc_text(PAGE, FakeGfxCard_FindAdapter) #pragma alloc_text(PAGE, FakeGfxCard_Initialize) #pragma alloc_text(PAGE, FakeGfxCard_StartIO) /********************************************************************** * * FakeGfxCard_ResetHW * * This routine would reset the hardware when a soft reboot is * performed. Returning FALSE from this routine would force * the HAL to perform an INT 10h and set Mode 3 (Text). * * We are not real hardware so we will just return TRUE so the HAL * does nothing. * **********************************************************************/ BOOLEAN FakeGfxCard_ResetHW(PVOID HwDeviceExtension, ULONG Columns, ULONG Rows) { return TRUE; } /********************************************************************** * * FakeGfxCard_VidInterrupt * * Checks if it's adapter generated an interrupt and dismisses it * or returns FALSE if it did not. * **********************************************************************/ BOOLEAN FakeGfxCard_VidInterrupt(PVOID HwDeviceExtension) { return FALSE; } /********************************************************************** * * FakeGfxCard_GetPowerState * * Queries if the device can support the requested power state. * **********************************************************************/ VP_STATUS FakeGfxCard_GetPowerState(PVOID HwDeviceExtension, ULONG HwId, PVIDEO_POWER_MANAGEMENT VideoPowerControl) { return NO_ERROR; } /********************************************************************** * * FakeGfxCard_SetPowerState * * Sets the power state. * **********************************************************************/ VP_STATUS FakeGfxCard_SetPowerState(PVOID HwDeviceExtension, ULONG HwId, PVIDEO_POWER_MANAGEMENT VideoPowerControl) { return NO_ERROR; } /********************************************************************** * * FakeGfxCard_GetChildDescriptor * * Returns an identifer for any child device supported * by the miniport. * **********************************************************************/ ULONG FakeGfxCard_GetChildDescriptor (PVOID HwDeviceExtension, PVIDEO_CHILD_ENUM_INFO ChildEnumInfo, PVIDEO_CHILD_TYPE pChildType, PVOID pChildDescriptor, PULONG pUId, PULONG pUnused) { return ERROR_NO_MORE_DEVICES; } /********************************************************************** * * FakeGfxCard_FindAdapter * * This function performs initialization specific to devices * maintained by this miniport driver. * **********************************************************************/ VP_STATUS FakeGfxCard_FindAdapter(PVOID HwDeviceExtension, PVOID HwContext, PWSTR ArgumentString, PVIDEO_PORT_CONFIG_INFO ConfigInfo, PUCHAR Again) { return NO_ERROR; } /********************************************************************** * * FakeGfxCard_Initialize * * This initializes the device. * **********************************************************************/ BOOLEAN FakeGfxCard_Initialize(PVOID HwDeviceExtension) { return TRUE; } /********************************************************************** * * FakeGfxCard_StartIO * * This routine executes requests on behalf of the GDI Driver * and the system. The GDI driver is allowed to issue IOCTLs * which would then be sent to this routine to be performed * on it's behalf. * * We can add our own proprietary IOCTLs here to be processed * from the GDI driver. * **********************************************************************/ BOOLEAN FakeGfxCard_StartIO(PVOID HwDeviceExtension, PVIDEO_REQUEST_PACKET RequestPacket) { RequestPacket->StatusBlock->Status = 0; RequestPacket->StatusBlock->Information = 0; return TRUE; }
由于我没有任何硬件,我只实现足够多的微型端口来让系统正常运行。我唯一打算使用的 API 是“StartIO”,如果我需要访问或执行显示驱动程序无法通过其有限的 API 集完成的操作。但是,在此实现中,我们不需要做任何事情。请记住,微型端口的主要目的是枚举硬件设备/资源并对其进行管理。如果您没有任何硬件,那么除了让驱动程序模型保持正常运行所需的最低限度之外,其他所有内容都将被移除。
显示驱动程序
显示驱动程序链接到 *WIN32K.SYS*,并且只允许调用 Eng* API。这些 API 实际上存在于内核和用户模式中。在 NT4 之前,显示驱动程序是在用户模式下的。无论如何,显示驱动程序使用的 API 集也由打印机驱动程序使用。遵循此 API 集还允许显示驱动程序通过最少的工作量转移到用户模式或内核模式。
但是,显示驱动程序不会加载到系统内存中,而是加载到会话空间。会话空间是内核中进程隔离的等价物。在用户模式下,进程有自己的虚拟内存地址空间;在内核模式下,会话有自己的虚拟内存地址空间。系统空间是内核内存,对所有会话都是全局的。
会话是已登录用户的实例,其中包含自己的窗口管理器、桌面、外壳和应用程序。这在 Windows XP 的“快速用户切换”中尤为明显,您可以在一台机器上登录多个用户。每个用户实际上都位于一个唯一的会话中,该会话有一个唯一的内核内存范围,称为会话空间。
这在设计视频驱动程序时可能是一个问题。这意味着您不能简单地将随机内存传递给您的微型端口,如果您的微型端口可能在当前会话之外处理该内存。例如,这是将此内存传递给另一个可能位于系统进程中的线程进行处理。
如果系统进程未与您的会话关联,您将访问与您认为的不同内存范围。发生这种情况时,您会看到“驱动程序未能正确移植到终端服务”的蓝屏。
显示驱动程序与我们迄今为止处理过的驱动程序完全不同。它仍然是 PE 格式,但它不像微型端口那样是一个普通的内核驱动程序,链接到不同的框架。此驱动程序不能通过直接链接到它们来使用内核 API,并且由于上述原因,它不应使用它们。如果 API 将内存传递到会话空间之外,则会出现蓝屏,除非您确保只传递系统内存。这也是只使用 Eng* API 集的另一个原因,尽管您可以从微型端口驱动程序请求函数指针表;没有任何东西可以阻止您这样做。
无论如何,显示驱动程序比普通驱动程序更像 DLL,并且基本上被视为 DLL。此驱动程序的框架与 *WIN32K.SYS* 相关联,该文件实现了窗口管理器以及 GDI。此驱动程序使用“` -entry:DrvEnableDriver@12 /SUBSYSTEM:NATIVE`”编译,其中 `DrvEnableDriver` 是显示驱动程序的入口点。
DrvEnableDriver
这是显示驱动程序的初始入口点,它与 `DriverEntry` 没有任何关系。此 API 传递一个 DRVENABLEDATA
结构,该结构需要用一组函数填充,这些函数是驱动程序的入口点。该表包含一个列表,其中包含一个索引值后跟函数指针。索引值指定函数类型,例如“`INDEX_DrvCompletePDEV”表示函数指针是指向驱动程序中 `DrvCompletePDEV 处理程序的指针。某些 API 是可选的,有些是必需的。
此入口点仅负责返回您的函数列表。您还可以在此处执行任何必要的初始化。以下是本文示例显示驱动程序中的代码
/* * Display Drivers provide a list of function entry points for specific GDI * tasks. These are identified by providing a pre-defined "INDEX" value (pre- * defined * by microsoft) followed by the function entry point. There are levels of * flexibility * on which ones you are REQUIRED and which ones are technically OPTIONAL. * */ DRVFN g_DrvFunctions[] = { { INDEX_DrvAssertMode, (PFN) GdiExample_DrvAssertMode }, { INDEX_DrvCompletePDEV, (PFN) GdiExample_DrvCompletePDEV }, { INDEX_DrvCreateDeviceBitmap, (PFN) GdiExample_DrvCreateDeviceBitmap }, { INDEX_DrvDeleteDeviceBitmap, (PFN) GdiExample_DrvDeleteDeviceBitmap }, { INDEX_DrvDestroyFont, (PFN) GdiExample_DrvDestroyFont }, { INDEX_DrvDisablePDEV, (PFN) GdiExample_DrvDisablePDEV }, { INDEX_DrvDisableDriver, (PFN) GdiExample_DrvDisableDriver }, { INDEX_DrvDisableSurface, (PFN) GdiExample_DrvDisableSurface }, { INDEX_DrvSaveScreenBits, (PFN) GdiExample_DrvSaveScreenBits }, { INDEX_DrvEnablePDEV, (PFN) GdiExample_DrvEnablePDEV }, { INDEX_DrvEnableSurface, (PFN) GdiExample_DrvEnableSurface }, { INDEX_DrvEscape, (PFN) GdiExample_DrvEscape }, { INDEX_DrvGetModes, (PFN) GdiExample_DrvGetModes }, { INDEX_DrvMovePointer, (PFN) GdiExample_DrvMovePointer }, { INDEX_DrvNotify, (PFN) GdiExample_DrvNotify }, // { INDEX_DrvRealizeBrush, (PFN) GdiExample_DrvRealizeBrush }, { INDEX_DrvResetPDEV, (PFN) GdiExample_DrvResetPDEV }, { INDEX_DrvSetPalette, (PFN) GdiExample_DrvSetPalette }, { INDEX_DrvSetPointerShape, (PFN) GdiExample_DrvSetPointerShape }, { INDEX_DrvStretchBlt, (PFN) GdiExample_DrvStretchBlt }, { INDEX_DrvSynchronizeSurface, (PFN) GdiExample_DrvSynchronizeSurface }, { INDEX_DrvAlphaBlend, (PFN) GdiExample_DrvAlphaBlend }, { INDEX_DrvBitBlt, (PFN) GdiExample_DrvBitBlt }, { INDEX_DrvCopyBits, (PFN) GdiExample_DrvCopyBits }, { INDEX_DrvFillPath, (PFN) GdiExample_DrvFillPath }, { INDEX_DrvGradientFill, (PFN) GdiExample_DrvGradientFill }, { INDEX_DrvLineTo, (PFN) GdiExample_DrvLineTo }, { INDEX_DrvStrokePath, (PFN) GdiExample_DrvStrokePath }, { INDEX_DrvTextOut, (PFN) GdiExample_DrvTextOut }, { INDEX_DrvTransparentBlt, (PFN) GdiExample_DrvTransparentBlt }, }; ULONG g_ulNumberOfFunctions = sizeof(g_DrvFunctions) / sizeof(DRVFN); /********************************************************************* * DrvEnableDriver * * This is the initial driver entry point. This is the "DriverEntry" * equivlent for Display and Printer drivers. This function must * return a function table that represents all the supported entry * points into this driver. * *********************************************************************/ BOOL DrvEnableDriver(ULONG ulEngineVersion, ULONG ulDataSize, DRVENABLEDATA *pDrvEnableData) { BOOL bDriverEnabled = FALSE; /* * We only want to support versions > NT 4 * */ if(HIWORD(ulEngineVersion) >= 0x3 && ulDataSize >= sizeof(DRVENABLEDATA)) { pDrvEnableData->iDriverVersion = DDI_DRIVER_VERSION; pDrvEnableData->pdrvfn = g_DrvFunctions; pDrvEnableData->c = g_ulNumberOfFunctions; bDriverEnabled = TRUE; } return bDriverEnabled; }
DrvDisableDriver
卸载显示驱动程序时会调用此函数处理程序。在此处理程序中,您可以执行 `DrvEnableDriver 调用中创建的任何必要的清理工作。以下代码来自示例驱动程序
/********************************************************************* * GdiExample_DrvDisableDriver * * This function is used to notify the driver when the driver is * getting ready to be unloaded. * *********************************************************************/ VOID GdiExample_DrvDisableDriver(VOID) { /* * No Clean up To Do */ }
DrvGetModes
驱动程序加载并启用后调用的 API 是 `DrvGetModes。此 API 用于查询设备支持的模式。这些模式用于填充“显示属性”对话框中的“设置”选项卡。模式可以缓存,以便操作系统不会认为它们是动态变化的。操作系统认为这是一个静态列表,尽管有时以及可以通过某些方式多次调用此 API,但大多数情况下不应将其视为动态的。
该 API 通常会调用两次:第一次调用仅用于获取存储模式所需的缓冲区大小,第二次调用会传入正确的缓冲区大小。以下代码片段来自仅支持 640x480x32 的示例驱动程序
/********************************************************************* * GdiExample_DrvGetModes * * This API is used to enumerate display modes. * * This driver only supports 640x480x32 * *********************************************************************/ ULONG GdiExample_DrvGetModes(HANDLE hDriver, ULONG cjSize, DEVMODEW *pdm) { ULONG ulBytesWritten = 0, ulBytesNeeded = sizeof(DEVMODEW); ULONG ulReturnValue; ENGDEBUGPRINT(0, "GdiExample_DrvGetModes\r\n", NULL); if(pdm == NULL) { ulReturnValue = ulBytesNeeded; } else { ulBytesWritten = sizeof(DEVMODEW); memset(pdm, 0, sizeof(DEVMODEW)); memcpy(pdm->dmDeviceName, DLL_NAME, sizeof(DLL_NAME)); pdm->dmSpecVersion = DM_SPECVERSION; pdm->dmDriverVersion = DM_SPECVERSION; pdm->dmDriverExtra = 0; pdm->dmSize = sizeof(DEVMODEW); pdm->dmBitsPerPel = 32; pdm->dmPelsWidth = 640; pdm->dmPelsHeight = 480; pdm->dmDisplayFrequency = 75; pdm->dmDisplayFlags = 0; pdm->dmPanningWidth = pdm->dmPelsWidth; pdm->dmPanningHeight = pdm->dmPelsHeight; pdm->dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFLAGS | DM_DISPLAYFREQUENCY; ulReturnValue = ulBytesWritten; } return ulReturnValue; }
DrvEnablePDEV
一旦选择了模式,就会调用此 API,该 API 将允许驱动程序启用“物理设备”。此 API 的目的是允许显示驱动程序创建自己的私有上下文,该上下文将传递给其他显示入口点。之所以需要此私有上下文,是因为单个显示驱动程序可能处理多个显示设备,因此需要区分一个显示设备与另一个设备。此 API 的返回值是指向所提供的显示设备的上下文或实例的指针。
选定的显示设置通过 `DEVMODE 参数传递到此 API,但示例驱动程序不使用此方法,因为它硬编码为仅设置 800x600x32 模式。
除了创建实例结构外,此 API 还必须至少初始化 `GDIINFO 和 `DEVINFO 数据结构。这些参数很重要,因为如果您填写了支持某种功能但实际上不支持,可能会导致图形损坏或甚至蓝屏。接下来我将提到的两个参数是 `hDev 和 `hDriver 参数。“`hDriver”参数实际上是显示驱动程序的 `DEVICE_OBJECT,可用于 `EngDeviceIoControl 等 API 与微型端口驱动程序通信。
`hDev 是 GDI 的句柄,但由于设备正在创建过程中,因此实际上是无用的。建议您等到 `DrvCompletePDEV 调用后再保存和使用此句柄。以下代码来自示例驱动程序的 `DrvEnablePDEV
/********************************************************************* * GdiExample_DrvEnablePDEV * * This function will provide a description of the Physical Device. * The data returned is a user defined data context to be used as a * handle for this display device. * * The hDriver is a handle to the miniport driver associated with * this display device. This handle can be used to communicate to * the miniport through APIs to send things like IOCTLs. * *********************************************************************/ DHPDEV GdiExample_DrvEnablePDEV(DEVMODEW *pdm, PWSTR pwszLogAddr, ULONG cPat, HSURF *phsurfPatterns, ULONG cjCaps, GDIINFO *pGdiInfo, ULONG cjDevInfo, DEVINFO *pDevInfo, HDEV hdev, PWSTR pwszDeviceName, HANDLE hDriver) { PDEVICE_DATA pDeviceData = NULL; ENGDEBUGPRINT(0, "GdiExample_DrvEnablePDEV Enter \r\n", NULL); pDeviceData = (PDEVICE_DATA) EngAllocMem(0, sizeof(DEVICE_DATA), FAKE_GFX_TAG); if(pDeviceData) { memset(pDeviceData, 0, sizeof(DEVICE_DATA)); memset(pGdiInfo, 0, cjCaps); memset(pDevInfo, 0, cjDevInfo); { pGdiInfo->ulVersion = 0x5000; pGdiInfo->ulTechnology = DT_RASDISPLAY; pGdiInfo->ulHorzSize = 0; pGdiInfo->ulVertSize = 0; pGdiInfo->ulHorzRes = RESOLUTION_X; pGdiInfo->ulVertRes = RESOLUTION_Y; pGdiInfo->ulPanningHorzRes = 0; pGdiInfo->ulPanningVertRes = 0; pGdiInfo->cBitsPixel = 8; pGdiInfo->cPlanes = 4; pGdiInfo->ulNumColors = 20; pGdiInfo->ulVRefresh = 1; pGdiInfo->ulBltAlignment = 1; pGdiInfo->ulLogPixelsX = 96; pGdiInfo->ulLogPixelsY = 96; pGdiInfo->flTextCaps = TC_RA_ABLE; pGdiInfo->flRaster = 0; pGdiInfo->ulDACRed = 8; pGdiInfo->ulDACGreen = 8; pGdiInfo->ulDACBlue = 8; pGdiInfo->ulAspectX = 0x24; pGdiInfo->ulNumPalReg = 256; pGdiInfo->ulAspectY = 0x24; pGdiInfo->ulAspectXY = 0x33; pGdiInfo->xStyleStep = 1; pGdiInfo->yStyleStep = 1; pGdiInfo->denStyleStep = 3; pGdiInfo->ptlPhysOffset.x = 0; pGdiInfo->ptlPhysOffset.y = 0; pGdiInfo->szlPhysSize.cx = 0; pGdiInfo->szlPhysSize.cy = 0; pGdiInfo->ciDevice.Red.x = 6700; pGdiInfo->ciDevice.Red.y = 3300; pGdiInfo->ciDevice.Red.Y = 0; pGdiInfo->ciDevice.Green.x = 2100; pGdiInfo->ciDevice.Green.y = 7100; pGdiInfo->ciDevice.Green.Y = 0; pGdiInfo->ciDevice.Blue.x = 1400; pGdiInfo->ciDevice.Blue.y = 800; pGdiInfo->ciDevice.Blue.Y = 0; pGdiInfo->ciDevice.AlignmentWhite.x = 3127; pGdiInfo->ciDevice.AlignmentWhite.y = 3290; pGdiInfo->ciDevice.AlignmentWhite.Y = 0; pGdiInfo->ciDevice.RedGamma = 20000; pGdiInfo->ciDevice.GreenGamma = 20000; pGdiInfo->ciDevice.BlueGamma = 20000; pGdiInfo->ciDevice.Cyan.x = 1750; pGdiInfo->ciDevice.Cyan.y = 3950; pGdiInfo->ciDevice.Cyan.Y = 0; pGdiInfo->ciDevice.Magenta.x = 4050; pGdiInfo->ciDevice.Magenta.y = 2050; pGdiInfo->ciDevice.Magenta.Y = 0; pGdiInfo->ciDevice.Yellow.x = 4400; pGdiInfo->ciDevice.Yellow.y = 5200; pGdiInfo->ciDevice.Yellow.Y = 0; pGdiInfo->ciDevice.MagentaInCyanDye = 0; pGdiInfo->ciDevice.YellowInCyanDye = 0; pGdiInfo->ciDevice.CyanInMagentaDye = 0; pGdiInfo->ciDevice.YellowInMagentaDye = 0; pGdiInfo->ciDevice.CyanInYellowDye = 0; pGdiInfo->ciDevice.MagentaInYellowDye = 0; pGdiInfo->ulDevicePelsDPI = 0; pGdiInfo->ulPrimaryOrder = PRIMARY_ORDER_CBA; pGdiInfo->ulHTPatternSize = HT_PATSIZE_4x4_M; pGdiInfo->flHTFlags = HT_FLAG_ADDITIVE_PRIMS; pGdiInfo->ulHTOutputFormat = HT_FORMAT_32BPP; *pDevInfo = gDevInfoFrameBuffer; pDevInfo->iDitherFormat = BMF_32BPP; } pDeviceData->pVideoMemory = EngMapFile(L"\\??\\c:\\video.dat", RESOLUTION_X*RESOLUTION_Y*4, &pDeviceData->pMappedFile); pDeviceData->hDriver = hDriver; pDevInfo->hpalDefault = EngCreatePalette(PAL_BITFIELDS, 0, NULL, 0xFF0000, 0xFF00, 0xFF); } ENGDEBUGPRINT(0, "GdiExample_DrvEnablePDEV Exit \r\n", NULL); return (DHPDEV)pDeviceData; }
DrvCompletePDEV
此调用在启用后进行,用于通知显示驱动程序设备对象已完成。唯一的参数是启用调用中创建的私有数据结构以及 GDI 设备的已完成句柄。除非您有更多初始化工作要做,否则通常只需保存 GDI 句柄即可。以下是示例驱动程序中的代码
/********************************************************************* * GdiExample_DrvCompletePDEV * * This is called to complete the process of enabling the device. * * *********************************************************************/ void GdiExample_DrvCompletePDEV(DHPDEV dhpdev, HDEV hdev) { PDEVICE_DATA pDeviceData = (PDEVICE_DATA)dhpdev; ENGDEBUGPRINT(0, "GdiExample_DrvCompletePDEV Enter \r\n", NULL); pDeviceData->hdev = hdev; ENGDEBUGPRINT(0, "GdiExample_DrvCompletePDEV Exit \r\n", NULL); }
DrvDisablePDEV
不再需要 PDEV 时以及 PDEV 将被销毁时会调用此 API。在 `DrvDisableSurface(如果启用了表面)之后调用此 API。我们对此 API 的实现非常简单,只会对创建私有 PDEV 结构期间创建的内容进行一些清理。
/********************************************************************* * GdiExample_DrvDisablePDEV * * This is called to disable the PDEV we created. * * *********************************************************************/ void GdiExample_DrvDisablePDEV(DHPDEV dhpdev) { PDEVICE_DATA pDeviceData = (PDEVICE_DATA)dhpdev; UINT dwBytesReturned = 0; ENGDEBUGPRINT(0, "GdiExample_DrvDisablePDEV\r\n", NULL); if(pDeviceData->pMappedFile) { EngUnmapFile(pDeviceData->pMappedFile); } EngFreeMem(dhpdev); }
DrvEnableSurface
在 PDEV 完成后调用此 API,要求显示驱动程序创建表面。另外,如下面的注释所述,您在创建表面时有两个选择。您可以创建一个由显示驱动程序管理的表面,也可以创建一个由 GDI 为您管理的表面。以下代码选择了管理自己的设备表面的选项。
其全部目的是定义一个 GDI 也可以在其上绘制的绘图表面。显示驱动程序有自己的设备表面,因此通常希望管理其表面。这样做时,它必须以 GDI 可以理解并能够在其上绘制的方式描述表面。这意味着要定义起始地址,甚至要定义间距,因为显示驱动程序通常不像所有模式那样具有线性缓冲区。在我们的例子中,我们使用我们创建的内存映射文件作为我们的视频内存。
/********************************************************************* * GdiExample_DrvEnableSurface * * This API is used to enable the physical device surface. * * You have two choices here. * * 1. Driver Manages it's own surface * EngCreateDeviceSurface - Create the handle * EngModifySurface - Let GDI Know about the object. * * 2. GDI Manages the surface * EngCreateBitmap - Create a handle in a format that * GDI Understands * EngAssociateSurface - Let GDI Know about the object. * * *********************************************************************/ HSURF GdiExample_DrvEnableSurface(DHPDEV dhpdev) { HSURF hsurf; SIZEL sizl; PDEVICE_DATA pDeviceData = (PDEVICE_DATA)dhpdev; ENGDEBUGPRINT(0, "GdiExample_DrvEnableSurface\r\n", NULL); pDeviceData->pDeviceSurface = (PDEVICE_SURFACE)EngAllocMem(FL_ZERO_MEMORY, sizeof(DEVICE_SURFACE), FAKE_GFX_TAG); sizl.cx = 800; sizl.cy = 600; hsurf = (HSURF)EngCreateDeviceSurface( (DHSURF)pDeviceData->pDeviceSurface, sizl, BMF_32BPP); EngModifySurface(hsurf, pDeviceData->hdev, HOOK_FILLPATH | HOOK_STROKEPATH | HOOK_LINETO | HOOK_TEXTOUT | HOOK_BITBLT | HOOK_COPYBITS, MS_NOTSYSTEMMEMORY, (DHSURF)pDeviceData->pDeviceSurface, pDeviceData->pVideoMemory, 800*4, NULL); return(hsurf); }
DrvDisableSurface
在 `DrvEnableSurface 调用中创建绘图表面时调用此 API 来销毁它。在销毁 PDEV 之前调用此 API。以下是示例程序中的代码
/********************************************************************* * GdiExample_DrvDisableSurface * * This API is called to disable the GDI Surface. * * *********************************************************************/ void GdiExample_DrvDisableSurface(DHPDEV dhpdev) { PDEVICE_DATA pDeviceData = (PDEVICE_DATA)dhpdev; ENGDEBUGPRINT(0, "GdiExample_DrvDisableSurface\r\n", NULL); EngDeleteSurface(pDeviceData->hsurf); pDeviceData->hsurf = NULL; EngFreeMem(pDeviceData->pDeviceSurface); pDeviceData->pDeviceSurface = NULL; }
排序
为了清晰起见,让我们再回顾一遍。
DrvEnableDriver
:驱动程序已加载。DrvGetModes
:获取用于存储所有支持的显示模式的缓冲区大小。DrvGetModes
:获取显示模式。DrvEnablePDEV
:通知显示驱动程序初始化为 `DEVMODE 数据结构中选定的模式,并返回实例句柄。DrvCompletePDEV
:通知驱动程序设备初始化已完成。DrvEnableSurface
:让驱动程序提供绘图表面。<GDI 调用>
DrvDisableSurface
:销毁绘图表面。DrvDisablePDEV
:销毁实例结构。DrvDisableDriver
:卸载显示驱动程序。
那么绘图是如何工作的?
“GDI 调用”实际上是在您的显示驱动程序中处理诸如“BitBlt
”之类的操作,而该操作实际上是在 `DrvBitBlt 中。您可能会注意到,我们的驱动程序本身并未实现任何图形命令。这是因为我们没有硬件来加速绘图功能,而且我决定调用 Windows 提供的例程要省力得多,这些例程已经在软件中实现了这些功能。例如,`DrvBitBlt 可以直接重定向到 `EngBitBlt。这些将直接渲染到我们的视频缓冲区,在我们的例子中,这是一个内存映射文件。
您可能会想,“我如何从这些 Drv* 调用中获取我的 PDEV 或我的表面对象?”嗯,传递给这些 API 的 `SURFOBJ 确实包含一个指向表面对象的指针。这些位于 `SURFOBJ 结构中的 `dhsurf 和 `dhpdev 成员。如果 `SURFOBJ 代表设备管理的表面,则 `dhsurf 成员是设备提供的句柄。这可以通过检查 `SURFOBJ 上设置的 `STYPE_DEVICE 标志来确定。
显示驱动程序转义码
在我的设备驱动程序教程中,我们了解到可以使用用户模式的“`DeviceIoControl”来实现我们自己的命令并在应用程序和驱动程序之间进行通信。这对于显示驱动程序也是可能的,尽管方式略有不同,它们被称为“转义码”而不是“IOCTL”。
在用户模式下,您可以使用以下两种方法之一向显示驱动程序发送“转义码”。第一种是 `ExtEscape,它只是将您提供的数据发送到驱动程序。您的显示驱动程序将在其 `DrvEscape 例程中处理此操作。
第二种方法是 `DrawEscape,可以在驱动程序中的 `DrvDrawEscape 中处理。区别在于,`DrawEscape 允许您提供一个窗口 DC 以及您的数据,并且该窗口的剪裁区域将被提供给您的驱动程序。这使得您可以轻松实现扩展绘图命令,这些命令可以在窗口环境中正确运行,因为您的驱动程序将了解正确的剪裁区域。
OpenGL 支持
OpenGL 支持是通过使用“ICD”或“可安装客户端驱动程序”来实现的。这是 SGI 最初创建的一个概念,旨在通过让供应商完全实现图形管线来提高 OpenGL 在 Windows 上的性能。当加载 *OpenGL32.DLL* 时,它会向视频驱动程序询问其 ICD,如果存在,它将被加载到进程空间中,并且 OpenGL API 将由 ICD 提供服务。ICD 完全控制图形管线,因此每个供应商和驱动程序版本可能都有不同的实现。
通常的做法是缓冲 OpenGL 命令,然后使用 `ExtEscape API 将它们刷新到显卡。ICD 工具包现在由 Microsoft 维护,如果您想为其进行开发,它不是免费的。
支持 OpenGL 的另一种方法是通过称为“微型客户端驱动程序”或“MCD”的东西。这是 Microsoft 最初用于 OpenGL 支持的方法,类似于 ICD,但 MCD 存在于内核中。据我所知,没有任何驱动程序供应商使用此方法,而且它非常慢,这就是 ICD 实现的原因。
DirectX 支持
在 XPDM 中,Direct Draw 支持是在 GDI 驱动程序中完成的。这是通过 `DrvEnableDirectDraw 接口实现的。DirectX 图形管线在用户模式部分和部分内核由 Microsoft 提供的系统组件实现。API 将简单地返回一个回调接口列表,DirectDraw 层在内核中使用这些接口来执行硬件中的特定操作。
Direct3D 通过 `DrvGetDirectDrawInfo 初始化,其中 GDI 驱动程序将声明支持 Direct3D。提供的回调将多次调用以获取驱动程序中实现 Direct3D 各项功能的适当接口。这在 MSDN 上有介绍。
什么是镜像驱动程序?
镜像驱动程序是一个文档不太完善的功能,您可以加载一个视频驱动程序来“镜像”另一个显示驱动程序。也就是说,它们将接收与它们镜像的显示驱动程序相同的调用。文档规定镜像驱动程序不支持 `DrvGetModes,但如果您实现了它,返回的模式将被缓存,您无法动态更改模式。尽管我听说实现 `DrvGetModes 可以帮助在模式切换时加载和卸载显示驱动程序,但我未能使其正常工作。
要加载镜像驱动程序,需要将此设备的注册表项中的“`Attach.ToDesktop”值设置为 1,然后使用“`CDS_UPDATEREGISTRY”在镜像驱动程序上调用 `ChangeDisplaySettingsEx。然后设置您希望切换到的模式,并再次在镜像驱动程序上调用 `ChangeDisplaySettingsEx。
镜像驱动程序在模式切换时不会正确卸载,并且通常如果存在对绘图表面的引用,则驱动程序不会卸载。因此,根据我的经验,要使镜像驱动程序能够进行模式切换,您需要一个能够检测 `WM_DISPLAYCHANGE 消息的应用程序。您还需要在加载显示驱动程序后将“`Attach.ToDesktop”设置为 0。这将有助于卸载显示驱动程序,在 `WM_DISPLAYCHANGE 时,您可以执行卸载镜像驱动程序的步骤。
如果您希望在不进行显示更改的情况下立即卸载镜像驱动程序,只需按照加载它的步骤进行即可。将“`Attach.ToDesktop”设置为 0,然后执行“`CDS_UPDATEREGISTRY”。然后您可以再次调用“`ChangeDisplaySettingsEx”而无需任何参数来强制卸载。尽管这似乎再次起作用,但一切都通过引用显示表面来完成,因此如果存在对显示表面的未决引用,则驱动程序不会被卸载。DDK 中的镜像驱动程序示例并未完成所有这些工作,并且缺少一些部分,例如未实现 `WM_DISPLAYCHANGE 并且在加载镜像驱动程序后未重置“`Attach.ToDesktop”值。
示例
本文中的示例驱动程序仅在应用程序和显示驱动程序之间共享一个内存映射文件。显示驱动程序会将图形命令写入内存映射文件,而应用程序只是充当监视器,大约每秒刷新一次自身。这效率不高,但这只是一个示例。显示驱动程序安装为普通的硬件驱动程序,就像 ATI 或 NVIDIA 驱动程序一样。
要安装示例,您只需使用控制面板中的“添加新硬件”向导。您必须选择“硬件已安装”和“从列表中手动选择硬件”。下图显示了设备列表,您向下滚动到底部并选择“添加新硬件设备”。
然后,您只需选择“有磁盘”并找到此项目随附的 *.INF* 文件。然后,您需要在此新列表中向下滚动并找到“Toby Opferman Sample Video Display”,如下图所示。
安装时会看到以下对话框,只需选择“继续”即可,除非您不想安装驱动程序。接下来,您只需使用显示设置和第三个选项卡启用第二个显示器。运行本文提供的监视器程序,您将在该应用程序窗口中看到第二个显示器。
家庭作业
阅读和观察是学习的好方法,但我认为如果您实际尝试做一些事情,您会学到更多!我希望您做的是采用我的示例并添加更多显示模式!这需要对应用程序进行更改,您可以尝试让应用程序通过各种方法(包括 `WM_DISPLAYCHANGE)检测这些显示更改,或者只需要求用户重新启动应用程序并提示或枚举设备以获取新的显示设置并相应地调整窗口。
这里有一个小提示。当选择新模式时,您并不总是会得到 `DrvDisableSurface、`DrvDisablePDEV,然后是新设置上的 `DrvEnablePDEV。您可能会得到 `DrvAssertMode。此 API 用于在 PDEV 之间切换,因为它会传递一个 BOOL 以通知驱动程序启用或禁用提供的 PDEV。
结论
本文介绍了如何创建非常基本的显示驱动程序来处理 GDI 命令。文章中提到的显示驱动程序架构仅涵盖 XPDM,而不是 Windows Vista 中的新 LDDM。这基本上也是“从哪里开始”的极端基础。即使如此,希望您对显示驱动程序和 Windows 操作系统有所了解。