Windows虚拟显示器MttVDD源码分析 (3) 驱动回调与入口点 (WDF/IddCx Callbacks)

在上一章 EDID与显示器模拟 中,我们成功地为虚拟显示器伪造了一张完美的"身份证"(EDID),并让 Windows 系统承认了它的存在。现在,我们的虚拟显示器已经在设备管理器中"榜上有名"了。

但是,这仅仅是第一步。Windows 知道有这么个显示器,但它还不知道该如何与我们的驱动程序"交谈"。当用户想要更改分辨率、将桌面图像发送过来,或者断开这个显示器时,Windows 该如何通知我们呢?

这就是本章的核心:建立驱动程序与操作系统之间的联系------回调函数 (Callbacks)

餐厅服务员的比喻

想象一下,你开了一家高科技餐厅。你不会让顾客直接闯进厨房对厨师大喊大叫,对吧?你需要一个高效的沟通系统。

  • 操作系统 (Windows):就是你的顾客。
  • 你的驱动程序 (MttVDD):就是你的餐厅后厨,拥有处理各种事务的能力。
  • 回调函数 (Callbacks):就是你雇佣的专业服务员团队。

你预先为每个岗位都安排好了服务员:

  • 一位"迎宾员" (DriverEntry):当餐厅开门(驱动加载)时,他负责开灯、检查设备。
  • 一位"领位员" (VirtualDisplayDriverDeviceAdd):当有新"设备"(比如虚拟显卡)需要服务时,他负责安排座位。
  • 一位"点餐员" (VirtualDisplayDriverMonitorQueryModes):当顾客想知道菜单(支持的分辨率)时,他负责解答。
  • 一位"上菜员" (VirtualDisplayDriverMonitorAssignSwapChain):当后厨做好了菜(桌面图像),他负责把菜(图像数据)端到顾客桌上。

这些"服务员"(回调函数)平时都在待命。一旦顾客(操作系统)发出特定的请求,对应的服务员就会被调用 (call),并立即开始执行他被赋予的任务。这种"被动响应"的模式,就是驱动程序工作的核心机制。

万物的起点:DriverEntry

DriverEntry 是我们驱动程序中最特殊、最重要的函数。它不是由我们自己调度的,而是当 Windows 决定加载 MttVDD 驱动时,由系统内核亲自调用的第一个函数。它就像是餐厅早晨打开大门的总开关,是所有工作的起点。

DriverEntry 的主要职责有两件:

  1. 进行全局初始化:就像我们在 配置与设置管理 章节中看到的,它负责加载所有配置文件,让驱动"记住"自己的设置。
  2. 注册下一位"服务员" :它告诉 Windows 驱动程序框架 (WDF, Windows Driver Framework),"你好,我已经准备好了。如果以后你发现了一个我应该管理的设备,请呼叫我的'领位员'------VirtualDisplayDriverDeviceAdd 函数。"

让我们看看 Driver.cppDriverEntry 函数的简化版代码:

cpp 复制代码
// Driver.cpp

extern "C" NTSTATUS DriverEntry(
	PDRIVER_OBJECT  pDriverObject,
	PUNICODE_STRING pRegistryPath
)
{
	WDF_DRIVER_CONFIG Config;
	WDF_OBJECT_ATTRIBUTES Attributes;

	// 初始化驱动配置,并告诉 WDF 框架
	// 当需要添加新设备时,应该调用哪个函数
	WDF_DRIVER_CONFIG_INIT(&Config, VirtualDisplayDriverDeviceAdd);

	// 加载各种设置...
	logsEnabled = EnabledQuery(L"LoggingEnabled");
	// ...

	// 创建驱动对象,完成注册
	WdfDriverCreate(pDriverObject, pRegistryPath, &Attributes, &Config, WDF_NO_HANDLE);

	return STATUS_SUCCESS;
}

这段代码的核心是 WDF_DRIVER_CONFIG_INIT(&Config, VirtualDisplayDriverDeviceAdd);。这行代码就像是在 WDF 这个"总调度中心"的登记簿上写下:"设备添加事件 -> 联系 VirtualDisplayDriverDeviceAdd"。

准备接待"顾客":VirtualDisplayDriverDeviceAdd

DriverEntry 成功运行后,我们的驱动就进入了待命状态。当 Windows 系统认为它"发现"了一个由我们驱动管理的虚拟显卡设备时,WDF 框架就会履行承诺,调用我们刚才注册的 VirtualDisplayDriverDeviceAdd 函数。

这个函数就像是"领位员",它的职责是为这个新设备做好一切准备工作。其中最重要的一项任务,就是向更专业的显示驱动框架 (IddCx, Indirect Display Driver Class eXtension) 注册一大批与显示相关的"专业服务员"。

IddCx 是专门为我们这种虚拟显示驱动设计的框架,它定义了所有与显示器交互的"标准流程"。

cpp 复制代码
// Driver.cpp

NTSTATUS VirtualDisplayDriverDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT pDeviceInit)
{
	// ... 其他初始化代码 ...

	IDD_CX_CLIENT_CONFIG IddConfig;
	// 初始化 IddCx 配置结构体
	IDD_CX_CLIENT_CONFIG_INIT(&IddConfig);

	// 向 IddCx 注册各种显示相关的回调函数
	// 当适配器初始化完成时,请调用...
	IddConfig.EvtIddCxAdapterInitFinished = VirtualDisplayDriverAdapterInitFinished;
	// 当需要分配渲染表面时,请调用...
	IddConfig.EvtIddCxMonitorAssignSwapChain = VirtualDisplayDriverMonitorAssignSwapChain;
	// 当需要释放渲染表面时,请调用...
	IddConfig.EvtIddCxMonitorUnassignSwapChain = VirtualDisplayDriverMonitorUnassignSwapChain;
	// ... 还有很多其他的回调注册 ...

	// 告诉 IddCx 我们的配置
	IddCxDeviceInitConfig(pDeviceInit, &IddConfig);

	// ... 创建设备对象 ...
	
	return STATUS_SUCCESS;
}

看到了吗?这个函数的核心工作就是填写一张巨大的"服务员联系表" (IddConfig),然后把它交给 IddCx 这个"显示部门经理"。从此以后,所有与显示相关的具体任务,都将由 IddCx 直接调度对应的回调函数来处理。

MttVDD 的"回调全家桶"

MttVDD 实现了很多回调函数,它们共同构成了驱动的核心逻辑。我们不需要一次性了解所有,但理解几个关键的就足以明白其工作原理:

回调函数 (服务员) 触发时机 (顾客请求) 核心任务
VirtualDisplayDriverAdapterInitFinished IddCx 已准备好虚拟显卡 调用 CreateMonitor 函数,正式向系统"插入"我们在配置文件中定义的虚拟显示器。这里会用到上一章的 EDID。
VirtualDisplayDriverMonitorQueryModes 系统询问:"这个显示器支持哪些分辨率和刷新率?" 读取我们从配置文件加载的显示模式列表(monitorModes 变量),并把它们报告给系统。用户在显示设置里看到的分辨率选项就来源于此。
VirtualDisplayDriverMonitorAssignSwapChain 系统决定要在这个显示器上显示内容了 这是最关键的回调之一!系统会给我们一个"交换链 (SwapChain)",它就像一个特殊的"画布"。从这一刻起,系统会源源不断地把桌面图像绘制到这个"画布"上。我们的驱动需要创建一个 交换链处理器 (SwapChainProcessor) 来接收这些图像。
VirtualDisplayDriverMonitorUnassignSwapChain 系统决定停止在这个显示器上显示内容(例如,用户断开连接) 收到通知后,我们需要停止接收图像,并清理与"交换链"相关的资源,就像服务员在顾客走后收拾桌子一样。
VirtualDisplayDriverEvtIddCxMonitorSetGammaRamp 用户在系统中调整颜色、亮度或伽马值 系统会发来新的颜色校准数据(Gamma Ramp)。我们的驱动需要接收这些数据,以便在处理图像时应用正确的颜色校正。这与 高级色彩与HDR管理 章节密切相关。

内部工作流程:一个完整的请求之旅

现在,我们把所有部分串联起来,看看从驱动加载到屏幕显示,这套回调系统是如何协同工作的。

下面的序列图展示了一个简化的流程:

sequenceDiagram participant OS as Windows 操作系统 participant WDF as WDF 框架 participant IddCx as IddCx 显示框架 participant MttVDD as MttVDD 驱动代码 OS->>WDF: 指令:加载 MttVDD.dll WDF->>MttVDD: 调用 DriverEntry() MttVDD-->>WDF: 注册 DeviceAdd 回调 Note over OS, MttVDD: 稍后,系统"发现"了虚拟显卡 WDF->>MttVDD: 调用 VirtualDisplayDriverDeviceAdd() MttVDD-->>IddCx: 注册所有显示相关的回调
(AssignSwapChain 等) Note over OS, MttVDD: 再稍后,用户在设置中启用了这个虚拟显示器 OS->>IddCx: 请求在此显示器上输出桌面 IddCx->>MttVDD: 调用 VirtualDisplayDriverMonitorAssignSwapChain() MttVDD->>MttVDD: 创建 SwapChainProcessor 开始接收图像

这个流程清晰地展示了"注册"与"调用"的分层关系:

  1. 启动层DriverEntry 向 WDF 注册设备添加回调。
  2. 设备层VirtualDisplayDriverDeviceAdd 向 IddCx 注册所有具体的显示功能回调。
  3. 功能层 :当特定事件发生时,IddCx 直接调用已注册的功能回调,如 AssignSwapChain

代码深潜:回调函数在哪里定义?

如果你打开 Driver.cpp 文件,你会在文件的顶部看到一长串函数声明。这些就是我们即将实现的所有回调函数的"签名"。

cpp 复制代码
// Driver.cpp

// ... (省略头文件包含)

// WDF 驱动/设备级别回调
extern "C" DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_DEVICE_ADD VirtualDisplayDriverDeviceAdd;

// IddCx 适配器级别回调
EVT_IDD_CX_ADAPTER_INIT_FINISHED VirtualDisplayDriverAdapterInitFinished;
EVT_IDD_CX_ADAPTER_COMMIT_MODES VirtualDisplayDriverAdapterCommitModes;

// IddCx 显示器级别回调
EVT_IDD_CX_MONITOR_ASSIGN_SWAPCHAIN VirtualDisplayDriverMonitorAssignSwapChain;
EVT_IDD_CX_MONITOR_UNASSIGN_SWAPCHAIN VirtualDisplayDriverMonitorUnassignSwapChain;
EVT_IDD_CX_MONITOR_QUERY_TARGET_MODES VirtualDisplayDriverMonitorQueryModes;
// ... 以及更多

// ... (文件的其余部分是这些函数的具体实现)

EVT_WDF_DRIVER_DEVICE_ADDEVT_IDD_CX_... 这些看起来奇怪的名字,实际上是 WDF 和 IddCx 框架预先定义好的函数指针类型。它们精确地规定了每个回调函数应该接受什么参数,以及应该返回什么类型的值。我们必须严格按照这些"模板"来编写我们的函数,否则框架就不知道如何正确地调用它们。

这种"填空式"的编程模型是 Windows 驱动开发的核心。框架已经搭建好了所有复杂的流程,我们作为驱动开发者,只需要根据这些预设的事件点,填写我们自己的逻辑代码即可。

总结

在本章中,我们深入了解了连接 MttVDD 驱动与 Windows 操作系统的桥梁------回调函数。我们学到了:

  • 回调是什么:它们是预先定义好的、由系统在特定事件发生时调用的函数,就像是餐厅里各司其职的服务员。
  • DriverEntry 的重要性:它是驱动程序的唯一入口点,负责全局初始化和注册最基础的设备添加回调。
  • 分层注册机制:驱动首先向 WDF 框架注册,然后在设备添加时,再向更专业的 IddCx 显示框架注册一系列详细的回调函数。
  • 关键回调的作用 :我们了解了像 AssignSwapChain(分配画布以接收图像)和 QueryModes(报告支持的分辨率)等核心回调的职责。
  • 事件驱动模型:驱动程序的大部分时间都在"等待"系统的调用,而不是主动执行。这是一个被动响应的编程模型。

现在我们已经搭建好了驱动与系统沟通的框架。我们知道系统会在什么时候、通过哪个函数来与我们对话。但是,当这些回调函数被调用时,它们如何访问和管理我们虚拟设备的状态呢?比如,AssignSwapChain 回调如何知道要为哪个显示器工作?它需要的数据(如渲染设备句柄)又存放在哪里?

这些状态和数据都集中存储在一个核心对象中。在下一章,我们将探索驱动的"中央数据库"------间接设备上下文 (IndirectDeviceContext)。

相关推荐
小xin过拟合23 分钟前
day20 二叉树part7
开发语言·数据结构·c++·笔记·算法
EstrangedZ32 分钟前
vscode(MSVC)进行c++开发的时,在debug时查看一个eigen数组内部的数值
c++·ide·vscode
乌萨奇也要立志学C++1 小时前
【C++详解】哈希表概念与实现 开放定址法和链地址法、处理哈希冲突、哈希函数介绍
c++·哈希算法·散列表
易我数据恢复大师2 小时前
怎么把iphone文件传输到windows电脑?分场景选方法
windows·iphone·iphone文件传输·iphone文件传输到电脑·iphone传输文件
Forward♞2 小时前
Qt——网络通信(UDP/TCP/HTTP)
开发语言·c++·qt
青草地溪水旁2 小时前
`lock()` 和 `unlock()` 线程同步函数
linux·c++·c
重启的码农3 小时前
Windows虚拟显示器MttVDD源码分析 (4) 间接设备上下文 (IndirectDeviceContext)
c++·windows·操作系统
重启的码农3 小时前
Windows虚拟显示器MttVDD源码分析 (2) EDID与显示器模拟
c++·windows·操作系统
重启的码农3 小时前
Windows虚拟显示器MttVDD源码分析 (1) 配置与设置管理
c++·windows·操作系统