欢迎来到 MttVDD
的世界!这是一个强大的虚拟显示驱动程序项目,可以让你在 Windows 系统中创造出"看不见"的显示器。在本系列教程中,我们将一步步揭开它神秘的面纱。
作为我们的第一站,我们将探索 MttVDD
的"大脑记忆库"------配置与设置管理系统。
为什么需要配置管理?
想象一下,你买了一台新电脑,但它的所有设置,比如屏幕亮度、音量大小、桌面壁纸,全都被焊死在了主板上,永远无法更改。这听起来是不是很糟糕?
驱动程序也是一样。如果没有一个灵活的配置系统,MttVDD
创造的虚拟显示器数量、支持的分辨率、是否开启 HDR 等特性都将被"写死"在代码里。每次你想创建一个 4K 而不是 1080p 的显示器,都必须修改代码、重新编译、然后重新安装驱动。这显然太麻烦了!
配置与设置管理 系统就是为了解决这个问题而存在的。它允许驱动程序在启动时,从外部文件(主要是 vdd_settings.xml
)和 Windows 注册表中读取所有配置,就像电脑每次开机都会加载你保存好的设置一样。这使得驱动程序的行为可以被用户轻松定制,实现了高度的灵活性。
核心概念:配置的来源
MttVDD
的配置主要有两个来源,它们之间存在一个优先级关系:
-
vdd_settings.xml
文件(主要来源) :这是最常用、最推荐的配置方式。它是一个位于驱动安装目录(通常是C:\VirtualDisplayDriver
)下的 XML 文件,你可以用任何文本编辑器打开并修改它。它的结构清晰,易于理解。 -
Windows 注册表(高优先级来源) :这是一个更高级的配置方式。存储在注册表中的设置项会覆盖
vdd_settings.xml
文件中的同名设置。这通常用于安装程序自动配置或由配套的控制软件进行临时修改,普通用户很少直接操作它。
这个"注册表优先"的设计非常巧妙。比如,vdd_settings.xml
里设置了创建 1 个显示器,但你可以通过配套软件下达一个临时指令,在注册表中写入"创建 3 个显示器",驱动重启后就会创建 3 个。当你撤销这个临时指令(删除注册表项)后,驱动又会恢复读取 XML 中的设置,变回 1 个显示器。
MttVDD
是如何加载设置的?
当 MttVDD
驱动程序被加载时,它会像一个勤劳的图书管理员,立即开始整理它的"记忆库"。这个过程主要发生在 Driver.cpp
文件的 DriverEntry
函数中,这是驱动程序的第一个入口点。
1. 将设置载入"内存"
驱动会调用一系列查询函数,比如 EnabledQuery
、GetIntegerSetting
等,从配置文件和注册表中读取每一项设置,并把它们存放在全局变量中。这些全局变量就像是大脑中的短期记忆,在驱动运行期间随时可以被访问。
来看看 Driver.cpp
中定义这些"短期记忆"的地方:
cpp
// Driver.cpp
// ... (省略其他代码)
// 全局变量,用于存储加载的设置
UINT numVirtualDisplays; // 要创建的虚拟显示器数量
bool logsEnabled = false; // 是否启用日志
bool debugLogs = false; // 是否启用调试日志
bool HDRPlus = false; // 是否开启 HDR+
// ... 还有很多其他的设置项
// ... (省略其他代码)
这些变量在被赋予从文件中读取的值之前,都有一个默认值(例如 false
或 0
)。
2. DriverEntry
:一切的起点
现在,让我们看看 DriverEntry
函数是如何启动这个加载过程的。这是一个简化版的代码片段:
cpp
// Driver.cpp - DriverEntry 函数
extern "C" NTSTATUS DriverEntry(
PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pRegistryPath
)
{
// ... (省略了WDF驱动初始化代码)
// 从配置文件和注册表加载各种开关设置
logsEnabled = EnabledQuery(L"LoggingEnabled");
debugLogs = EnabledQuery(L"DebugLoggingEnabled");
customEdid = EnabledQuery(L"CustomEdidEnabled");
// 加载颜色相关的设置
HDRPlus = EnabledQuery(L"HDRPlusEnabled");
SDR10 = EnabledQuery(L"SDR10Enabled");
// ... (加载更多设置)
// ... (省略了创建驱动对象的代码)
return Status;
}
正如你所见,DriverEntry
函数在执行任何重要操作之前,就通过 EnabledQuery
等函数把配置项一个个地加载到对应的全局变量中。这样,驱动程序在其生命周期内的任何时刻,都可以通过访问 logsEnabled
、HDRPlus
等变量来决定自己的行为。
深入探索:设置是如何被查询的
我们已经知道驱动通过 EnabledQuery
这样的函数来获取设置,但这些函数内部又是如何工作的呢?它们如何知道要去哪里查找,以及如何处理优先级?
答案藏在一个名为 SettingsQueryMap
的"字典"里。
SettingsQueryMap
:配置的翻译官
SettingsQueryMap
是一个 C++ std::map
结构,它定义在 Driver.cpp
的顶部。它就像一个翻译官,告诉查询函数,一个内部的设置名(比如 LoggingEnabled
)对应到注册表和 XML 文件里分别叫什么名字。
cpp
// Driver.cpp
std::map<std::wstring, std::pair<std::wstring, std::wstring>> SettingsQueryMap = {
// 内部名称 {注册表名称, XML标签名}
{L"LoggingEnabled", {L"LOGS", L"logging"}},
{L"DebugLoggingEnabled", {L"DEBUGLOGS", L"debuglogging"}},
{L"HDRPlusEnabled", {L"HDRPLUS", L"HDRPlus"}},
// ... 更多设置项
};
有了这张"翻译表",EnabledQuery(L"LoggingEnabled")
就知道:
- 在注册表里,我应该找一个叫
LOGS
的值。 - 在 XML 文件里,我应该找一个名叫
<logging>
的标签。
查询流程揭秘
现在,让我们用一个流程图来展示 EnabledQuery
函数的完整工作流程。假设我们要查询 LoggingEnabled
这个设置。
找到 XML 名 "logging" Query->>Reg: 尝试读取 "LOGS" 的值 alt 注册表中存在 "LOGS" Reg-->>Query: 返回 "LOGS" 的值 (例如 1) Query-->>Driver: 返回 true else 注册表中不存在 Reg-->>Query: 未找到 Query->>XML: 尝试读取
这个流程清晰地展示了"注册表优先"的原则。接下来,我们看看简化后的代码实现。
cpp
// Driver.cpp - EnabledQuery 函数的简化逻辑
bool EnabledQuery(const std::wstring& settingKey) {
// 1. 从 SettingsQueryMap 中查找注册表和 XML 的名字
auto it = SettingsQueryMap.find(settingKey);
std::wstring regName = it->second.first;
std::wstring xmlName = it->second.second;
// 2. 尝试从注册表读取
HKEY hKey;
LONG lResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"...", 0, KEY_READ, &hKey);
if (lResult == ERROR_SUCCESS) {
// ... 读取注册表值的代码 ...
if (成功读取) {
RegCloseKey(hKey);
return true; // 如果读取成功,直接返回
}
RegCloseKey(hKey);
}
// 3. 如果注册表没有,则从 XML 文件读取 (这是备用方案)
// ... 使用 IXmlReader 打开和解析 vdd_settings.xml 的代码 ...
// ... 寻找到名为 xmlName 的标签并读取其内容 ...
return xmlLoggingValue; // 返回从 XML 中读取的值
}
这个函数完美地实现了我们之前描述的逻辑。GetIntegerSetting
、GetStringSetting
等其他查询函数也遵循着类似的逻辑,只是它们返回的数据类型不同。
loadSettings
:加载显示模式
除了简单的开关和数值设置,驱动还需要知道要创建哪些显示模式(即分辨率和刷新率的组合)。这些复杂的列表式信息由 loadSettings
函数专门负责处理。
这个函数会在驱动设备被创建时(VirtualDisplayDriverDeviceAdd
内部)调用。它会解析 vdd_settings.xml
中关于显示器数量和分辨率的部分。
下面是一个 vdd_settings.xml
的例子:
xml
<!-- C:\VirtualDisplayDriver\vdd_settings.xml -->
<vdd_settings>
<displays>
<count>2</count> <!-- 创建2个显示器 -->
</displays>
<resolutions>
<resolution>
<width>1920</width>
<height>1080</height>
<refresh_rate>60</refresh_rate>
</resolution>
<resolution>
<width>2560</width>
<height>1440</height>
<refresh_rate>120</refresh_rate>
</resolution>
</resolutions>
<!-- ... 其他设置 ... -->
</vdd_settings>
loadSettings
函数会读取这个文件,然后:
- 将
numVirtualDisplays
全局变量设置为2
。 - 将两个分辨率和刷新率组合存入
monitorModes
这个全局vector
向量中。
之后,在创建显示器的阶段,驱动就会读取 numVirtualDisplays
来决定创建几个显示器,并使用 monitorModes
中的数据来告诉 Windows 每个显示器支持哪些模式。这部分内容将在后续的 EDID与显示器模拟
章节中详细介绍。
总结
在本章中,我们学习了 MttVDD
的配置与设置管理系统。我们了解到:
- 为什么需要配置:为了使驱动程序灵活可定制,避免硬编码。
- 配置的来源 :主要是
vdd_settings.xml
文件,同时支持使用 Windows 注册表进行高优先级覆盖。 - 加载时机 :所有配置在驱动启动时(
DriverEntry
)一次性加载到全局变量中,作为驱动运行期间的"记忆"。 - 核心机制 :通过
SettingsQueryMap
这个"翻译字典",查询函数(如EnabledQuery
)能够统一地从注册表和 XML 中读取设置。 - 专门的模式加载 :
loadSettings
函数负责从 XML 中读取复杂的显示模式列表。
这个强大的配置系统是 MttVDD
所有高级功能的基础。没有它,我们将无法自由地定制虚拟显示器的行为。
现在,我们已经知道了驱动是如何"记住"我们想要创建什么样的显示器的。在下一章中,我们将学习驱动是如何将这些愿望变成现实,并让 Windows 系统真正识别和相信我们创造出的虚拟显示器。