在上一章 EDID与显示器模拟 中,我们成功地为虚拟显示器伪造了一张完美的"身份证"(EDID),并让 Windows 系统承认了它的存在。现在,我们的虚拟显示器已经在设备管理器中"榜上有名"了。
但是,这仅仅是第一步。Windows 知道有这么个显示器,但它还不知道该如何与我们的驱动程序"交谈"。当用户想要更改分辨率、将桌面图像发送过来,或者断开这个显示器时,Windows 该如何通知我们呢?
这就是本章的核心:建立驱动程序与操作系统之间的联系------回调函数 (Callbacks)。
餐厅服务员的比喻
想象一下,你开了一家高科技餐厅。你不会让顾客直接闯进厨房对厨师大喊大叫,对吧?你需要一个高效的沟通系统。
- 操作系统 (Windows):就是你的顾客。
- 你的驱动程序 (
MttVDD
):就是你的餐厅后厨,拥有处理各种事务的能力。 - 回调函数 (Callbacks):就是你雇佣的专业服务员团队。
你预先为每个岗位都安排好了服务员:
- 一位"迎宾员" (
DriverEntry
):当餐厅开门(驱动加载)时,他负责开灯、检查设备。 - 一位"领位员" (
VirtualDisplayDriverDeviceAdd
):当有新"设备"(比如虚拟显卡)需要服务时,他负责安排座位。 - 一位"点餐员" (
VirtualDisplayDriverMonitorQueryModes
):当顾客想知道菜单(支持的分辨率)时,他负责解答。 - 一位"上菜员" (
VirtualDisplayDriverMonitorAssignSwapChain
):当后厨做好了菜(桌面图像),他负责把菜(图像数据)端到顾客桌上。
这些"服务员"(回调函数)平时都在待命。一旦顾客(操作系统)发出特定的请求,对应的服务员就会被调用 (call),并立即开始执行他被赋予的任务。这种"被动响应"的模式,就是驱动程序工作的核心机制。
万物的起点:DriverEntry
DriverEntry
是我们驱动程序中最特殊、最重要的函数。它不是由我们自己调度的,而是当 Windows 决定加载 MttVDD
驱动时,由系统内核亲自调用的第一个函数。它就像是餐厅早晨打开大门的总开关,是所有工作的起点。
DriverEntry
的主要职责有两件:
- 进行全局初始化:就像我们在 配置与设置管理 章节中看到的,它负责加载所有配置文件,让驱动"记住"自己的设置。
- 注册下一位"服务员" :它告诉 Windows 驱动程序框架 (WDF, Windows Driver Framework),"你好,我已经准备好了。如果以后你发现了一个我应该管理的设备,请呼叫我的'领位员'------
VirtualDisplayDriverDeviceAdd
函数。"
让我们看看 Driver.cpp
中 DriverEntry
函数的简化版代码:
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管理 章节密切相关。 |
内部工作流程:一个完整的请求之旅
现在,我们把所有部分串联起来,看看从驱动加载到屏幕显示,这套回调系统是如何协同工作的。
下面的序列图展示了一个简化的流程:
(AssignSwapChain 等) Note over OS, MttVDD: 再稍后,用户在设置中启用了这个虚拟显示器 OS->>IddCx: 请求在此显示器上输出桌面 IddCx->>MttVDD: 调用 VirtualDisplayDriverMonitorAssignSwapChain() MttVDD->>MttVDD: 创建 SwapChainProcessor 开始接收图像
这个流程清晰地展示了"注册"与"调用"的分层关系:
- 启动层 :
DriverEntry
向 WDF 注册设备添加回调。 - 设备层 :
VirtualDisplayDriverDeviceAdd
向 IddCx 注册所有具体的显示功能回调。 - 功能层 :当特定事件发生时,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_ADD
和 EVT_IDD_CX_...
这些看起来奇怪的名字,实际上是 WDF 和 IddCx 框架预先定义好的函数指针类型。它们精确地规定了每个回调函数应该接受什么参数,以及应该返回什么类型的值。我们必须严格按照这些"模板"来编写我们的函数,否则框架就不知道如何正确地调用它们。
这种"填空式"的编程模型是 Windows 驱动开发的核心。框架已经搭建好了所有复杂的流程,我们作为驱动开发者,只需要根据这些预设的事件点,填写我们自己的逻辑代码即可。
总结
在本章中,我们深入了解了连接 MttVDD
驱动与 Windows 操作系统的桥梁------回调函数。我们学到了:
- 回调是什么:它们是预先定义好的、由系统在特定事件发生时调用的函数,就像是餐厅里各司其职的服务员。
DriverEntry
的重要性:它是驱动程序的唯一入口点,负责全局初始化和注册最基础的设备添加回调。- 分层注册机制:驱动首先向 WDF 框架注册,然后在设备添加时,再向更专业的 IddCx 显示框架注册一系列详细的回调函数。
- 关键回调的作用 :我们了解了像
AssignSwapChain
(分配画布以接收图像)和QueryModes
(报告支持的分辨率)等核心回调的职责。 - 事件驱动模型:驱动程序的大部分时间都在"等待"系统的调用,而不是主动执行。这是一个被动响应的编程模型。
现在我们已经搭建好了驱动与系统沟通的框架。我们知道系统会在什么时候、通过哪个函数来与我们对话。但是,当这些回调函数被调用时,它们如何访问和管理我们虚拟设备的状态呢?比如,AssignSwapChain
回调如何知道要为哪个显示器工作?它需要的数据(如渲染设备句柄)又存放在哪里?
这些状态和数据都集中存储在一个核心对象中。在下一章,我们将探索驱动的"中央数据库"------间接设备上下文 (IndirectDeviceContext)。