Windows虚拟显示器MttVDD源码分析 (2) EDID与显示器模拟

在上一章 配置与设置管理 中,我们了解了 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 的方式:

  1. 使用内置的默认 EDID:驱动程序代码里已经预先写好了一份通用的 EDID 数据。这是最简单、最可靠的方式,足以应对大多数基本需求。
  2. 使用用户提供的自定义 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 函数的简化工作流程:

sequenceDiagram participant Driver as 驱动启动 participant MainCalc as maincalc() participant LoadEdid as loadEdid() participant File as user_edid.bin participant GlobalVar as 全局EDID变量 Driver->>MainCalc: 调用,准备EDID MainCalc->>LoadEdid: 请求EDID原材料 alt "CustomEdid" 已启用 LoadEdid->>File: 尝试读取 user_edid.bin File-->>LoadEdid: 返回文件数据 (如果存在) end LoadEdid-->>MainCalc: 返回EDID数据 (自定义或内置) MainCalc->>MainCalc: 计算并填写校验和 MainCalc->>GlobalVar: 存储最终完成的EDID

这个流程中有个非常关键的步骤:计算校验和 (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);
}

IddCxMonitorCreateIddCxMonitorArrival 被调用时,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 回调。

相关推荐
EstrangedZ7 分钟前
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
重启的码农2 小时前
Windows虚拟显示器MttVDD源码分析 (3) 驱动回调与入口点 (WDF/IddCx Callbacks)
c++·windows·操作系统
重启的码农2 小时前
Windows虚拟显示器MttVDD源码分析 (4) 间接设备上下文 (IndirectDeviceContext)
c++·windows·操作系统
重启的码农3 小时前
Windows虚拟显示器MttVDD源码分析 (1) 配置与设置管理
c++·windows·操作系统
Warren983 小时前
Appium学习笔记
android·windows·spring boot·笔记·后端·学习·appium