在上一章 配置与设置管理 中,我们了解了 MttVDD
是如何"记住"我们想要创建什么样的显示器的。我们通过修改配置文件,告诉了驱动程序要创建几个显示器,以及它们应该支持哪些分辨率。
但是,仅仅有这些想法还不够。我们如何让 Windows 这个"大管家"相信我们真的有一个显示器,并且愿意和它"对话"呢?这就是本章要解决的核心问题:创造一个足以以假乱真的虚拟显示器身份证。
为什么需要"身份证"?
想象一下,你走进一个需要身份验证才能进入的大楼。如果你没有身份证,保安会让你进去吗?当然不会。
Windows 系统对待显示器也是如此。当你把一个真实的显示器插入电脑时,它会立刻向 Windows 出示自己的"身份证"。这张"身份证"告诉 Windows:"你好,我是一台戴尔显示器,我的型号是 U2721DE,我最高支持 2560x1440 分辨率,刷新率最高 60Hz,我还支持 sRGB 色彩空间......"
这张特殊的"身份证"就叫做 EDID(Extended Display Identification Data,扩展显示标识数据)。
对于 MttVDD
创造的虚拟显示器来说,它没有物理实体,自然也拿不出这张"身份证"。因此,驱动程序的核心任务之一,就是凭空伪造一张完美的 EDID,然后郑重地交给 Windows。只有当 Windows 看到并相信了这张 EDID,它才会承认这个虚拟显示器的存在,并在显示设置中把它列出来。
EDID:虚拟显示器的灵魂
EDID 本质上是一块很小的数据,通常是 128 或 256 字节。但这小小的几百个字节里,却包含了显示器的所有关键信息。MttVDD
能否成功模拟显示器,完全取决于它提供的 EDID 是否"真实可信"。
MttVDD
提供了两种生成 EDID 的方式:
- 使用内置的默认 EDID:驱动程序代码里已经预先写好了一份通用的 EDID 数据。这是最简单、最可靠的方式,足以应对大多数基本需求。
- 使用用户提供的自定义 EDID:如果你想模拟一个特定的、具有高级功能(比如 HDR)的显示器,你可以提供一个自己的 EDID 文件,驱动会加载它。
1. 内置的"万能身份证"
在 Driver.cpp
文件中,你可以找到一个名为 hardcodedEdid
的变量。它就是一个 std::vector<BYTE>
(字节向量),里面存储了 MttVDD
默认的 EDID 数据。
cpp
// Driver.cpp
// 一个预先定义好的、通用的 EDID 数据
vector<BYTE> hardcodedEdid =
{
// 头部信息
0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
// 制造商和产品ID (这里是虚拟的)
0x36, 0x94, 0x37, 0x13, 0xe7, 0x1e, 0xe7, 0x1e,
// ... 省略了其他几十个字节的数据 ...
// 校验和 (最后一个字节,稍后会计算)
0x8c
};
这份 EDID 定义了一个通用的虚拟显示器,包含了像 1920x1080、2560x1440 等常见的分辨率和刷新率信息。对于初学者来说,你完全不需要关心它的具体内容,只需要知道这是驱动的"保底"方案。
2. 加载自定义"专属身份证"
MttVDD
的强大之处在于它的可定制性。通过在配置文件 vdd_settings.xml
中将 <CustomEdid>
设置为 true
,你可以让驱动加载一个你自己准备的 EDID 文件。
这个文件必须命名为 user_edid.bin
,并放在驱动的安装目录(通常是 C:\VirtualDisplayDriver
)下。
这个加载过程由 loadEdid
函数负责。它的逻辑非常直接:
cpp
// Driver.cpp - loadEdid 函数的简化逻辑
vector<BYTE> loadEdid(const string& filePath) {
// 检查 "CustomEdidEnabled" 设置是否为 true
if (customEdid) {
// ... 尝试从 filePath 读取 user_edid.bin 文件 ...
if (文件成功读取) {
vddlog("i", "正在使用自定义 EDID");
return 文件内容;
}
}
// 如果不使用自定义 EDID,或者文件读取失败,就返回内置的 EDID
vddlog("i", "正在使用内置的硬编码 EDID");
return hardcodedEdid;
}
这个设计非常灵活,它优先尝试满足用户的定制需求,同时保证在定制失败时总有一个可靠的备用方案。
内部实现:EDID 的诞生之旅
我们已经知道了 EDID 的来源,但一份原始的 EDID 数据是如何被处理并最终呈现在 Windows 面前的呢?这个过程的核心在一个叫做 maincalc
的函数里。
maincalc
:EDID 的"总装车间"
你可以把 maincalc
函数想象成一个"身份证总装车间"。当驱动启动时,它会被调用来完成 EDID 的最后准备工作。
下面是 maincalc
函数的简化工作流程:
这个流程中有个非常关键的步骤:计算校验和 (Checksum)。
什么是校验和?
想象一下,你在寄送一份有 127 个数字的重要文件。为了防止对方收到的数字有误,你在文件的末尾附上了第 128 个数字,这个数字是前面所有 127 个数字的总和。接收方收到后,会自己把前 127 个数字加一遍,然后和你附上的总和进行比较。如果两个数对得上,就说明文件在传输过程中没有出错。
EDID 的校验和就是这个"总和"。它确保了 EDID 数据的完整性和正确性。Windows 在接收到 EDID 时,会做的第一件事就是验证校验和。如果校验和错误,Windows 会直接拒绝这份 EDID,认为这个显示器"有问题"。
maincalc
函数会调用 calculateChecksum
函数,严格按照标准计算出正确的校验和,然后把它填写到 EDID 数据的最后一个字节(第 127 字节)上。
cpp
// Driver.cpp
int maincalc() {
// 1. 从 loadEdid 函数获取原始的 EDID 数据
vector<BYTE> edid = loadEdid(WStringToString(confpath) + "\\user_edid.bin");
// 2. (可选) 如果未禁用厂商信息伪造,则修改制造商信息
if (!preventManufacturerSpoof) {
modifyEdid(edid);
}
// 3. 计算正确的校验和
BYTE checksum = calculateChecksum(edid);
// 4. 将正确的校验和写入 EDID 的最后一个字节
edid[127] = checksum;
// 5. 将这份完美的 EDID 存入一个全局静态变量,等待被使用
IndirectDeviceContext::s_KnownMonitorEdid = edid;
return 0;
}
执行完 maincalc
后,一份合法、有效的"显示器身份证"就准备就绪了,它被存放在 IndirectDeviceContext::s_KnownMonitorEdid
中,随时可以"出示"。
CreateMonitor
:向 Windows 出示"身份证"
"身份证"造好了,下一步就是在合适的时机把它交给 Windows。这个时机就是驱动创建虚拟显示器的时候,具体发生在 IndirectDeviceContext::CreateMonitor
函数中。
这个函数负责与 Windows 的 IddCx (Indirect Display Driver Class eXtension) 框架交互,正式"注册"一个新的显示器。
cpp
// Driver.cpp - 在 IndirectDeviceContext::CreateMonitor 中
void IndirectDeviceContext::CreateMonitor(unsigned int index) {
// ... 其他准备工作 ...
// 准备一个描述显示器信息的结构体
IDDCX_MONITOR_INFO MonitorInfo = {};
MonitorInfo.MonitorDescription.Size = sizeof(MonitorInfo.MonitorDescription);
MonitorInfo.MonitorDescription.Type = IDDCX_MONITOR_DESCRIPTION_TYPE_EDID;
// 关键步骤:告诉 Windows 我们的"身份证"在哪里,有多大
MonitorInfo.MonitorDescription.DataSize = static_cast<UINT>(s_KnownMonitorEdid.size());
MonitorInfo.MonitorDescription.pData = IndirectDeviceContext::s_KnownMonitorEdid.data();
// ... 其他信息填充 ...
// 正式向 IddCx 提交创建请求,Windows 会读取并解析 pData 指向的 EDID
IDARG_OUT_MONITORCREATE MonitorCreateOut;
IddCxMonitorCreate(m_Adapter, &MonitorCreate, &MonitorCreateOut);
// 通知 Windows "显示器已插入"
IddCxMonitorArrival(m_Monitor, &ArrivalOut);
}
当 IddCxMonitorCreate
和 IddCxMonitorArrival
被调用时,Windows 就像那位大楼保安,它会接过我们递上的 s_KnownMonitorEdid
数据,仔细检查一番(包括验证校验和)。一旦确认无误,它就会高兴地认为:"哦,一个新的显示器连接上来了!" 随后,你就能在系统的显示设置里看到这个全新的虚拟显示器了。
总结
在本章中,我们揭开了虚拟显示器如何"欺骗"Windows 的秘密。我们学到了:
- EDID 是什么:它是显示器的"身份证",详细描述了显示器的各项能力。
- 为什么它很重要:没有一个合法有效的 EDID,Windows 根本不会承认虚拟显示器的存在。
- EDID 的来源 :
MttVDD
可以使用内置的hardcodedEdid
,也可以加载用户提供的user_edid.bin
文件,这为高级定制提供了可能。 - EDID 的准备过程 :
maincalc
函数是核心的"总装车间",它负责加载 EDID 数据并计算至关重要的校验和。 - EDID 的提交 :
CreateMonitor
函数在创建显示器时,通过 IddCx 框架将准备好的 EDID 呈报给 Windows,从而完成虚拟显示器的"注册"。
至此,我们的虚拟显示器已经在 Windows 系统中拥有了合法的"身份"。但是,仅仅被识别还不够,Windows 还需要知道如何与我们的驱动程序进行持续的沟通,例如:当用户更改分辨率时该怎么办?当需要开始传输桌面图像时又该怎么通知驱动?
这些问题都将通过一系列"回调函数"来解决。在下一章,我们将深入了解这些连接驱动与系统的"电话线"------WDF/IddCx 回调。