在上一章 驱动回调与入口点 (WDF/IddCx Callbacks)中,我们建立了驱动程序与 Windows 操作系统之间的联系------回调函数。我们现在知道,当系统需要我们做事时,它会呼叫我们指定的"服务员"(回调函数)。
但是,这里有一个问题:我们的"服务员"团队虽然知道在什么时候该做什么,但他们彼此之间是独立的。AssignSwapChain
回调如何知道要为哪个显示器工作?CreateMonitor
回调又如何知道它应该属于哪个虚拟显卡?如果每个"服务员"都各自为政,没有一个统一的指挥中心,整个"餐厅"很快就会陷入混乱。
这就是本章要介绍的核心概念------IndirectDeviceContext
,我们驱动程序的"总指挥部"。
为什么需要一个"总指挥"?
想象一个庞大的建筑项目。有负责打地基的团队,有负责砌墙的团队,还有负责安装水电的团队。如果他们之间没有任何沟通,也没有一个总项目经理来协调,会发生什么?墙可能砌在了错误的地基上,水电管道也可能穿过了不该穿的地方。
在驱动程序中,那些回调函数就像是这些独立的施工团队。IndirectDeviceContext
就是这个项目的总项目经理 或总指挥。
它是一个 C++ 类(class),我们为每一个创建的虚拟显卡设备都实例化一个 IndirectDeviceContext
对象。这个对象就像是为该项目专门设立的"指挥部办公室",它包含了与这个虚拟显卡相关的所有状态、数据和核心操作。
- 状态和数据 :它知道自己管理的虚拟显卡 (
m_Adapter
) 和虚拟显示器 (m_Monitor
) 的句柄(Handle)。 - 核心操作 :它定义了如何创建显示器 (
CreateMonitor
)、如何处理图像数据 (AssignSwapChain
) 等具体方法。
通过这种方式,当一个回调函数被系统调用时,它的首要任务就是找到对应的"总指挥部"(IndirectDeviceContext
实例),然后由这个总指挥来统一调度和执行任务。这确保了所有操作都是围绕着同一个虚拟设备进行的,避免了混乱。
"总指挥部"的蓝图:IndirectDeviceContext
类
IndirectDeviceContext
的设计蓝图定义在 Driver.h
头文件中。让我们来看一个简化版的定义,了解一下这个"指挥部"里都有哪些关键部门和文件。
cpp
// 文件: Driver.h
class IndirectDeviceContext
{
public:
// 构造函数:当"指挥部"建立时调用
IndirectDeviceContext(_In_ WDFDEVICE WdfDevice);
// 初始化虚拟显卡
void InitAdapter();
// 创建一个虚拟显示器
void CreateMonitor(unsigned int index);
// 分配交换链,准备接收图像
void AssignSwapChain(IDDCX_MONITOR& Monitor, IDDCX_SWAPCHAIN SwapChain, ...);
// 释放交换链,停止接收图像
void UnassignSwapChain();
protected:
// 指向 WDF 设备对象的句柄,像是通往上级(WDF框架)的电话线
WDFDEVICE m_WdfDevice;
// 虚拟显卡的句柄,像是项目的总档案
IDDCX_ADAPTER m_Adapter;
// 虚拟显示器的句柄,像是具体显示器的分档案
IDDCX_MONITOR m_Monitor;
// 指向交换链处理器的指针,这是负责处理图像数据的"专家"
std.unique_ptr<SwapChainProcessor> m_ProcessingThread;
public:
// 存储准备好的 EDID 数据
static std::vector<BYTE> s_KnownMonitorEdid;
};
这个类的结构非常清晰:
- 成员变量 (
m_...
) :这些是"指挥部"里存放的核心资料。m_WdfDevice
、m_Adapter
和m_Monitor
都是句柄 (Handle) ,你可以把它们理解为访问系统资源的"钥匙"或"ID卡"。例如,当你想对虚拟显卡进行操作时,你就需要出示m_Adapter
这张"ID卡"。m_ProcessingThread
则指向一个专门处理图像流的对象,我们将在下一章 交换链处理器 (SwapChainProcessor) 中详细探讨它。 - 成员函数/方法 :这些是"总指挥"能下达的命令,比如
InitAdapter()
("项目启动!")和CreateMonitor()
("开始建造一个新显示器!")。
"指挥部"的创建与关联
我们已经有了"总指挥部"的设计蓝-图,但它是在何时何地被建造起来的呢?又是如何与我们的虚拟设备关联起来的呢?
答案就在 VirtualDisplayDriverDeviceAdd
这个回调函数中。我们在上一章讲到,当系统"发现"我们的虚拟显卡设备时,会调用这个函数。这正是为这个新设备建立"指挥部"的最佳时机。
Windows 驱动框架 (WDF) 有一个非常巧妙的机制,叫做上下文空间 (Context Space) 。它允许我们把任何自定义的数据结构(比如我们的 IndirectDeviceContext
对象)"附加"到一个系统对象(比如 WDFDEVICE
)上。就像是给一个标准的政府大楼(WDFDEVICE
)旁边加盖一间我们自己的办公室。
1. 声明"办公室"类型
首先,我们需要告诉 WDF 框架,我们将要附加的"办公室"是什么样的。
cpp
// 文件: Driver.cpp
// 一个简单的包装结构,用于存放指向我们上下文对象的指针
struct IndirectDeviceContextWrapper
{
IndirectDeviceContext* pContext;
// ... 清理函数 ...
};
// 使用宏来声明这个包装结构可以作为 WDF 对象的上下文
WDF_DECLARE_CONTEXT_TYPE(IndirectDeviceContextWrapper);
2. 建造并附加"办公室"
然后,在 VirtualDisplayDriverDeviceAdd
函数中,我们进行实际的建造和附加操作。
cpp
// 文件: Driver.cpp - VirtualDisplayDriverDeviceAdd 函数内
NTSTATUS VirtualDisplayDriverDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT pDeviceInit)
{
// ... 其他初始化代码 ...
WDF_OBJECT_ATTRIBUTES Attr;
// 关键步骤:告诉 WDF 我们要为设备对象附加一个 IndirectDeviceContextWrapper 类型的上下文
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&Attr, IndirectDeviceContextWrapper);
WDFDEVICE Device = nullptr;
// 创建设备对象,并根据 Attr 的设置,为其预留上下文空间
WdfDeviceCreate(&pDeviceInit, &Attr, &Device);
// ... 其他初始化 ...
// 从刚创建的设备中,获取指向我们上下文空间的指针
auto* pContextWrapper = WdfObjectGet_IndirectDeviceContextWrapper(Device);
// 在上下文中,真正创建我们的"总指挥"对象
pContextWrapper->pContext = new IndirectDeviceContext(Device);
return STATUS_SUCCESS;
}
这个过程可以分解为三步:
- 规划 :通过
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE
告诉 WDF 我们需要一块地方来放IndirectDeviceContextWrapper
。 - 建造 :
WdfDeviceCreate
不仅创建了标准的设备对象,还在旁边为我们预留了那块"上下文空间"。 - 入驻 :通过
WdfObjectGet_IndirectDeviceContextWrapper
拿到这块空间的"钥匙",然后new IndirectDeviceContext(Device)
,正式让我们的"总指挥"入驻。
回调函数如何找到"总指挥"?
现在,我们的"总指挥部"已经和设备对象牢牢地绑定在了一起。当任何一个与显示相关的回调函数被 IddCx 框架调用时,系统总会提供一个句柄(比如 IDDCX_ADAPTER
或 IDDCX_MONITOR
)作为参数。
这些句柄本身也是 WDF 对象,它们都与最初的设备对象相关联。这意味着,我们可以从任何一个这样的句柄出发,找到我们附加的上下文。
下面的流程图展示了一个回调函数(例如 VirtualDisplayDriverAdapterInitFinished
)如何找到并命令"总指挥"工作的过程。
(例如,调用 CreateMonitor)
让我们看看实际的代码,这非常简单直接:
cpp
// 文件: Driver.cpp
// 当适配器初始化完成时,IddCx 调用此回调
NTSTATUS VirtualDisplayDriverAdapterInitFinished(
IDDCX_ADAPTER AdapterObject,
const IDARG_IN_ADAPTER_INIT_FINISHED* pInArgs
)
{
// 1. 通过传入的 AdapterObject 句柄,找到我们的"总指挥"
auto* pContextWrapper = WdfObjectGet_IndirectDeviceContextWrapper(AdapterObject);
IndirectDeviceContext* pDeviceContext = pContextWrapper->pContext;
if (NT_SUCCESS(pInArgs->AdapterInitStatus))
{
// 2. 命令"总指挥"完成剩下的初始化工作
pDeviceContext->FinishInit();
}
return STATUS_SUCCESS;
}
看到这个模式了吗?几乎所有的回调函数都遵循这个模式:
- 接收一个系统传入的句柄。
- 通过该句柄获取
IndirectDeviceContext
的实例。 - 调用该实例上的一个方法来执行实际的工作。
这种设计模式将无状态的 C 风格回调函数与面向对象的、有状态的 C++ 类完美地结合在了一起,使得驱动程序的逻辑清晰、易于管理。
总结
在本章中,我们认识了 MttVDD
驱动程序的核心大脑------IndirectDeviceContext
类。我们学到了:
- 为什么需要它:为了集中管理一个虚拟显卡设备的所有状态和操作,避免在分散的回调函数中造成混乱。它就像一个项目的"总指挥"。
- 它的构成 :它包含了指向 WDF 设备、虚拟显卡和显示器等核心资源的句柄(成员变量),以及操作这些资源的方法(成员函数)。
- 它的创建时机 :它在
VirtualDisplayDriverDeviceAdd
回调函数中被创建,并通过 WDF 的上下文空间 机制,"附加"到代表我们虚拟设备的WDFDEVICE
对象上。 - 如何使用它 :任何回调函数在被调用时,都可以通过系统传入的句柄,方便地获取到与之关联的
IndirectDeviceContext
实例,然后调用其方法来执行具体任务。
IndirectDeviceContext
为我们提供了一个坚实的框架,所有的功能都可以在这个"指挥部"中有条不紊地进行。它就像是舞台的总导演,而它所指挥的最重要的一个演员,就是负责处理每一帧桌面图像的"交换链处理器"。
在下一章中,我们将深入了解这个负责接收和处理真实桌面图像数据的关键组件。