写在开坑前
其实算算上次写技术文已经差不多过去三四个春秋了, 其实本来觉得像我这种没啥文采还废话连篇的下等作者早该尽早自行了断免得误了看官, 不曾想为了捧个人场又来这厚颜无耻, 既然坑开了, 那七七八八总得唠两句以示诚意.
为什么写 IDD?
讲真的, 最近能写的玩意儿真不少, 但是 IDD 是我觉得最值得一写的, 主要原因在于只答应了 披头哥 短短三篇, 所以这次开的 IDD 坑定然是草草三篇收场, 当然这已经足以参透 IDD 了.
如果有条件的话, 建议参考 Github 上的 Microsoft/Windows-driver-sample. 微软近几年的文档无论从完善度还是清晰度来说, 都非常值得作为 产品级 的参考基础. 当然了, 配合本文肯定能大大的减轻阅读负担.
我想要代码!
很抱歉, 我不能.
由于 IDD 所开发的项目已经商用, 所以我并不能提供完整的源代码, 但是如果结合上述的开源范例, 我相信你还是可以很轻松的开发出一个切实可用的 IDD 驱动.
章节一眼看
我大致的将 IDD 的实现按照功能分为下面的三个章节
- [本文] 初始化一个没有功能的 IDD 壳子.
- 通过 IDD 来创建显示器.
- 获取显示器的输出内容.
你可以根据当前对 IDD 的了解情况来选择性的进行阅读.
课前预习
本着以人为本的原则, 也为了避免半途而废带来的挫败感, 在开始阅读前, 我准备了一份简单的条件自测, 如果您满足这些条件, 那么恭喜你, 踩过这个坑对你来说可能不是难事, 如果你并不具备这些条件但是自诩智慧过人, 那么我相信硬着头皮看完之后可能就能 Get 到这些能力了.
- 使用 VC++ 完成一些中等复杂程度的应用开发.
- 听说过 DirectX 并且知道他是干啥的.
- 打开过 Windows "设备管理器", 知道如何通过 "设备管理器" 安装/更换设备驱动.
- 了解过 WDK 甚至用过 WDK 写一个啥功能都没的无聊驱动.
- 把电脑搞蓝屏过, 知道如何通过安全模式修好驱动导致的蓝屏.
- 知道 Microsoft(微软) 别名 Hugehard(巨硬)
- 基础的英文阅读能力, 或者掌握百度翻译的入门级使用.
好戏开场, 什么是 IDD ?
IDD 全名 Indirect Display Driver 是一个诞生于 2016 年的新家伙, 比起十数年前就已经面世的 WDDM 来说, 无论是 代码可读性 还是 逻辑清晰度 简直是 Too Young Too Simple, 再加上有 WDF 的加持, 开发显卡过滤驱动这件事情从未如此简单.
通过 IDD 我们可以做到许多曾经需要通过 不正经 方法才能实现的功能需求, 如: 驱动级屏幕采集, 虚拟显示器.
IDD 还有三秒到达战场
严格意义上来说, IDD 并不是一个可以独立工作的系统组件, 它更像是一个开发框架, 巨硬 帮助你做完了一些复杂的底层工作, 而你负责享受 框架 完成的各种便利接口.
IDD 本身基于 IndirectKmd.sys 驱动, 你可以方便的在 *\SystemRoot\System32\drivers* 中找到他的本体, 我们一切使用 IDD 开发的"驱动"都会先行加载这个驱动, 如果用一个不太负责的例子来说明的话
我们的驱动程序需要依赖 IndirectKmd.sys 这个动态链接库.
与世上其他的妖艳贱货应用一样的是, IDD 也有一套内部的执行顺序, 系统通过回调的方式将其串联, 我简单将其分为四大块: 电源管理, 显卡设备功能, 显示设备功能, 显示功能.
实际 IDD 在执行时, 遵照的顺序也差不多如下:
graph LR
创建WDF驱动-->创建WDF设备-->电源初始化:上电-->初始化显示设备-->初始化SwapChain
0. 万事开头难....吗? 简单的创建 WDF 驱动
就像一个正常的驱动一样, IDD 驱动的入口函数也是 DriverEntry, 所有在 DriverEntry 中线性执行的工作就叫他: 第 0 阶段吧.
三行伪代码便可以清楚地描述这个工作:
DriverEntry(driverObject, registryPath) {
attributes = WDF_OBJECT_ATTRIBUTES_INIT();
config = WDF_DRIVER_CONFIG_INIT(Device::Initialize);
WdfDriverCreate(driverObject, registryPath, attributes, config, WDF_NO_HANDLE);
}
如果足够的眼尖, 你肯定一眼就看出了下一步的回调方法 Device::Initialize. 系统会在完成 WDF 初始化后调用这个方法, 我们需要在这个函数当中提供设备功能以及电源管理的 回调函数表. 做完这些个之后, 一个合格的 WDF 驱动就算是完成了.
1. 先有鸡还是先有蛋? 创建 WDF 设备
我们通常认为: 先有设备再有驱动, 可是在 WDF 的体系中并不是这样的, 我们已经在上一步中创建了 WDF 驱动, 接下来需要一个 WDF 设备.
以下是一份 Device::Initialize 的伪代码:
Device::Initialize(driverObject, deviceInit) {
pnpPowerCallbacks = WDF_PNPPOWER_EVENT_CALLBACKS_INIT();
pnpPowerCallbacks.EvtDeviceD0Entry = Device::PowerOn;
WdfDeviceInitSetPnpPowerEventCallbacks(deviceInit, pnpPowerCallbacks);
iddFunctions = IDD_CX_CLIENT_CONFIG_INIT();
iddFunctions.EvtIddCxAdapterInitFinished = Device::Ready;
iddFunctions.EvtIddCxAdapterCommitModes = Device::CommitModes;
iddFunctions.EvtIddCxParseMonitorDescription = Monitor::ParseDescription;
iddFunctions.EvtIddCxMonitorGetDefaultDescriptionModes = Monitor::GetDefaultModes;
iddFunctions.EvtIddCxMonitorQueryTargetModes = Monitor::QueryModes;
iddFunctions.EvtIddCxMonitorAssignSwapChain = Monitor::AssignSwapChain;
iddFunctions.EvtIddCxMonitorUnassignSwapChain = Monitor::UnassignSwapChain;
IddCxDeviceInitConfig(deviceInit, iddFunctions);
device = WdfDeviceCreate(deviceInit);
IddCxDeviceInitialize(device)
}
显而易见的是, 我们在这个步骤做了两件事: 注册了电源回调 以及 注册了功能回调.
EvtDeviceD0Entry 事件是下一步的主角, D0 是设备上电状态, 其余事件我们可以暂且按下不表.
2. 来电了之后咋整? 起床干活!
既然有了电, 那是时候向系统展示我们真正的技术了, 这时候我们需要告知系统我们能做啥, 当然也得向系统汇报一下我们是谁.
Device::PowerOn(WDFDevice) {
capability.MaxMonitorsSupported = GRAPHIC_CARD_MAX_MONITORS;
capability.EndPointDiagnostics.GammaSupport = IDDCX_FEATURE_IMPLEMENTATION_NONE;
capability.EndPointDiagnostics.TransmissionType = IDDCX_TRANSMISSION_TYPE_WIRED_OTHER;
capability.EndPointDiagnostics.pEndPointFriendlyName = GRAPHIC_CARD_NAME;
capability.EndPointDiagnostics.pEndPointManufacturerName = GRAPHIC_CARD_MANUFACTURER;
capability.EndPointDiagnostics.pEndPointModelName = GRAPHIC_CARD_MODEL;
firmwareVersion.MajorVer = 2;
firmwareVersion.MinorVer = 0;
capability.EndPointDiagnostics.pFirmwareVersion.MajorVer = firmwareVersion;
capability.EndPointDiagnostics.pHardwareVersion.MinorVer = firmwareVersion;
init.WdfDevice = _WDFDevice;
init.pCaps = &capability;
IddCxAdapterInitAsync(init);
}
具体字段的用途就不阐述了, 有兴趣的可以自行百度翻译, 巨硬 的字段名还是相对比较容易理解的.
重点在于 IddCxAdapterInitAsync, 明眼人一看就知道, 这是一个异步方法, 异步方法就一定有回调, 那么回调是哪个呢?
显然帮助你们踩坑的我一直提前知道了答案, 那就是 EvtIddCxAdapterInitFinished, 对应着我们的 Device::Ready.
其实也好理解, 来电了, 设备初始化了, 下一步就是设备就绪了.
3. 玩家准备就绪! 全军出击.
这一步是设备初始化完成的最后一步, 也是下一章节的序幕, 我们需要在这里创建具体的显示设备, 也就是显示器.
我们在此祭出一段非常简单的代码方便下一章的开篇:
Device::Ready(adapterObject, pInArgs) {
while(MonitorsToCreate --) {
Monitor::Initialize(MonitorsToCreate, adapterObject);
}
}
上例中我们简单的创建固定数量的显示设备仅做范例, 在此先不展开具体细节只提一嘴: 你该准备一下要创建哪些显示器了.
大部分用于生产系统的应用程序都不可能将这些重要的参数 Hard-Coded 在二进制文件中, 所以您可以在文末思考一下从哪获取这些.
一个可行的方案是从配置文件读取, 只不过如何更新配置文件就留给你们自行发挥了.
末尾叨叨
作为闰更作者, 打算来个 Triple Kill 也是付出了老大的勇气的.
如果确实对文中的代码存疑或者确实需要帮助, 欢迎邮箱联系 nvmjs#soxos.me, 除了伸手要全套服务的, 我一定给到力所能及的帮助.
本文采用 CC BY-NC-SA 4.0 协议进行许可
原文地址: soxos.m2d.in/go/tzYAA. 首发于 nvmjs.com 且已经获得原作者许可.