开局一目录
我大致的将 IDD 的实现按照功能分为下面的三个章节
- 初始化一个没有功能的 IDD 壳子.
- 通过 IDD 来创建显示器.
- [本文] 获取显示器的输出内容与驱动的加载.
你可以根据当前对 IDD 的了解情况来选择性的进行阅读.
课前预习
本着以人为本的原则, 也为了避免半途而废带来的挫败感, 在开始阅读前, 我准备了一份简单的条件自测, 如果您满足这些条件, 那么恭喜你, 踩过这个坑对你来说可能不是难事, 如果你并不具备这些条件但是自诩智慧过人, 那么我相信硬着头皮看完之后可能就能 Get 到这些能力了.
- 使用 VC++ 完成一些中等复杂程度的应用开发.
- 听说过 DirectX 并且知道他是干啥的.
- 打开过 Windows "设备管理器", 知道如何通过 "设备管理器" 安装/更换设备驱动.
- 了解过 WDK 甚至用过 WDK 写一个啥功能都没的无聊驱动.
- 把电脑搞蓝屏过, 知道如何通过安全模式修好驱动导致的蓝屏.
- 知道 Microsoft(微软) 别名 Hugehard(巨硬)
- 基础的英文阅读能力, 或者掌握百度翻译的入门级使用.
书接上回
我们将会完成终极目标: 获取到那个看不见的显示器的内容, 以及一个必要的题外话: 怎么把驱动挂载到我们的操作系统中.
graph LR
创建WDF驱动-->创建WDF设备-->电源初始化:上电-->初始化显示设备-->SC((初始化SwapChain))
与人方便, 与己偷懒.
如果你足够仔细的阅读了上一章节, 应该发现咱们的回调方法似乎只剩下下面这两个方法还没用到了, 没错, 这次要用到的就是仅仅如下的两个简单方法:
iddFunctions.EvtIddCxMonitorAssignSwapChain = Monitor::AssignSwapChain;
iddFunctions.EvtIddCxMonitorUnassignSwapChain = Monitor::UnassignSwapChain;
但是如果你结合了巨硬官方示例代码来看本章节的话就会发现, 最关键的这两个方法在 示例 中居然 交了白卷!
所以如果你在结合 示例代码 的同时阅读前两章节比较困难的话, 简易重新回头再读一遍, 不然... 累.
0. 不积跬步无以至千里. 唠嗑一样的说说 SwapChain
SwapChain, 一个几乎没有歧义项的名词, 百度一下你可能就能知道个七七八八, 我们这里先一步到位的用一句简单的白话文总结一下:
SwapChain 就是存在于 显卡 中的用来存储即将呈现出来但是还没呈现的画面的一个 缓冲区(Buffer)
让我们先暂时忘掉咱们的显示器是虚拟的这件事, 无论你用的是啥型号是啥类型的显示器, 操作系统都会稳定负责的一帧一帧的渲染需要呈现的画面, 这些画面会被存储在 实际负责渲染的设备 内存中也就是 显存.
了解这点之后我们来看看上面这个句子里的关键词: 实际负责渲染的设备. 我们已经知道 IDD 是一个虚拟设备, 那么自然并不能承担实际渲染工作, 所以, 实际上我们的 SwapChain 依然存在于物理显卡上, 为了实现咱们不可告人的目的, 所以咱们需要把这部分数据取出. 为了实现这个小目标, 咱么又得了解一下 Windows 中 实际负责渲染的设备 到底是一个什么设备. 为了达成这个目的, 咱们另辟蹊径的来看看什么样的设备可以运行 Windows. 我从巨硬官网抄来的如下配置需求表:
处理器:1GHz或更快的处理器
内存:1GB(32位)或2GB(64位)
硬盘空间:16GB(32位操作系统)或20GB(64位操作系统)
显卡:DirectX 9或更高版本(包含WDDM 1.0驱动程序)
重点就在 DirectX. 在 Windows Vista 之后, 咱们亲爱的巨硬全面的使用 DirectX 来渲染不只是游戏也包括了桌面窗口的全部显示内容, 所以就目前来说, 一个可以提供 SwapChain 的设备那一定是一个 D3D 设备.
1. 真书接上回! 就由回调方法结束废话吧
时间来到 Monitor::AssignSwapChain, 我们终于可以创建心心念念的 SwapChain 处理器 了, 但是就像之前说的, 我们首先需要一个虚拟的 D3D 设备, 然后再处理 SwapChain.
那么统共几个步骤呢? 两个!
Monitor::AssignSwapChain(SwapChain, RenderAdapter, NewFrameEvent) {
Device = D3DDevice(RenderAdapter);
StartProcessSwapChain(SwapChain, Device, NewFrameEvent)
}
先别骂, 主要是这两个方法的行为天差地别, 咱们分开来看. 先瞧瞧 D3DDevice 的初始化:
D3DDevice::D3DDevice(RenderAdapter) {
D3DDevice::DxgiFactory = CreateDXGIFactory2(0);
D3DDevice::Adapter = DxgiFactory->EnumAdapterByLuid(RenderAdapter);
{D3DDevice::Device, D3DDevice::DeviceContext} = D3D11CreateDevice(D3DDevice::Adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, D3D11_SDK_VERSION, nullptr);
}
我们通过一系列的骚操作, 成功的创建了一个与 实际渲染设备 绑定的 D3D 设备 以及对应 上下文.
说实话, 这个步骤没啥大用途, 只是为了处理 SwapChain 的必经之路, 所以我们不赘述细节, 继续关注我们的重中之重 StartProcessSwapChain.
2. 终于来了! StartProcessSwapChain
在经历了半小时的脱裤等待后, 咱们终于到了激动人心的 StartProcessSwapChain 环节.
简单的概括一下处理流程的话, 我们接下来只要做 2 件事:
- 完成获取 SwapChain 前的准备
- 在一个循环中不停的 获取/释放 帧(Frame)
我们先将 StartProcessSwapChain 伪代码呈上:
StartProcessSwapChain(SwapChain, Device, NewFrameEvent) {
SetDevice.pDevice = DxgiDevice;
IddCxSwapChainSetDevice(SwapChain, &SetDevice);
while(1) {
NextFrame(SwapChain, Device)
}
}
忍住别动手, 要不是 NextFrame 打算细讲, 不然也不会那么敷衍 StartProcessSwapChain.
为表诚意, 咱们就不水章节, 直接来看 NextFrame:
NextFrame(SwapChain, Device) {
Buffer = IddCxSwapChainReleaseAndAcquireBuffer(SwapChain);
gpuImage = Buffer.MetaData.pSurface->QueryInterface(__uuidof(ID3D11Texture2D));
imageDescription = gpuImage->GetDesc();
imageDescription.Usage = D3D11_USAGE_STAGING;
imageDescription.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
imageDescription.BindFlags = 0;
imageDescription.MiscFlags = 0;
imageDescription.MipLevels = 1;
imageDescription.ArraySize = 1;
imageDescription.SampleDesc.Count = 1;
cpuImage = Device->CreateTexture2D(&imageDescription, NULL);
Device->CopyResource(cpuImage, gpuImage);
IddCxSwapChainFinishedProcessingFrame(SwapChain)
}
有点简单? 还真就那么简单. 但是其中原理还是很复杂的.
首先, 我们需要获取并锁定一帧的缓存, 也就是 IddCxSwapChainReleaseAndAcquireBuffer. 但是, 我们获取到的不只有帧的图像数据, 还有一些杂七杂八的我们无用的垃圾, 所以我们需要依靠 QueryInterface 来从集合中剥离图像数据由此得到了显存中的图像 gpuImage.
可惜的是如同前文所述, 这一帧存在于 显存 中, 我们并不能读取到他的具体内容就好像应用间的内存不能直接共享, 所以我们需要下一个步骤: 拷贝到系统内存. 为达此目的, 咱们需要知道图片的大小尺寸, 也就是 gpuImage->GetDesc.
现在的我们有了关于图像数据的一切信息, 同时我们为了达成可以直接访问图片的目的设置了一些必须的 标识符(Flag), 接下来我们依葫芦画瓢, 让 D3D 设备帮助我们创建一个 内存 中的图像, 为此我们需要拜托 Device->CreateTexture2D 并且让可以 男女通吃 同时穿梭于 内存 和 显存 间的 D3D 设备帮助我们把数据 CopyResource 到内存中.
最后, 过河拆桥, 我们使用 IddCxSwapChainFinishedProcessingFrame 放回资源数据, 让系统可以继续下一帧的渲染.
其实 SwapChain 的教程可以到此为止, 但是我们增加一个新的小目标来方便加深印象: 把屏幕数据存到文件中.
简单起见, 咱们采用 BMP 作为文件数据, 伪代码仅演示如何访问像素内存.
SaveToImage(cpuImage, imageDescription) {
cpuImage->Map(&Memory);
saveToBmp(Memory, imageDescription.Width, imageDescription.Height);
cpuImage->Unmap();
}
需要注意的是我们通过 Map 获得的数据是 BGRA 的数据序列, 为什么是 BGRA? 请自行百度 小端字节序.
3. 巧妇难为无米之炊? 收获前的种地
来, 让我们编译一下看看. 什么, 编译输出了一个 DLL 文件, 双击没法运行?
那可不是...
如果你曾经打开过 Windows 设备管理器, 那么应该知道为了正常的挂载驱动, 我们需要有一个设备. 可是咱们这虚头巴脑的玩意儿咋创建设备?
还得靠代码, 咱们另起一个新的控制台程序的炉灶, 来看看题外话: 创建一个虚拟设备.
int main() {
deviceInformation.cbSize = sizeof(deviceInformation);
deviceInformation.pszzCompatibleIds = "DEVICE_COMPATIBLE_ID";
deviceInformation.pszInstanceId = "DEVICE_ID";
deviceInformation.pszzHardwareIds = "DEVICE_HARDWARE_ID";
deviceInformation.pszDeviceDescription = "DEVICE_DESCRIPTION";
deviceInformation.CapabilityFlags = SWDeviceCapabilitiesRemovable | SWDeviceCapabilitiesSilentInstall | SWDeviceCapabilitiesDriverRequired
Event = CreateEvent(nullptr, FALSE, FALSE, nullptr);
SWDevice = SwDeviceCreate(TEXT(DEVICE_ID), L"HTREE\\ROOT\\0", &deviceInformation, 0, nullptr, Created, &Event);
}
那么下一个问题: SWDevice 是啥? 很简单 SoftWare Device 也就是软件设备.
这部分代码特别没啥变化, 我们仅仅是需要执行这个 exe 文件就完事了.
后面的事情相信你也会了. 什么, 你不会?
打开 设备管理器, 选择刚刚创建的这个设备, 右键更新驱动, 选择我们生成的驱动 inf 文件, 安装. 完事.
全文终
不用你们说我也知道, 通过这三章节的文章很难让一个没有驱动开发经验的小白创建出人生第一台虚拟现实器 但是如果仅仅是作为官方资料的补充, 我相信一定是可以的.
平心而论, IDD 真的不适合初学者作为入门选项, 我也并不是真的希望有人可以依葫芦画瓢的实现自己的 IDD 驱动, 更多的是想让那些已经依靠官方示例画完瓢的开发者能够更加深入了解 IDD 的工作流程乃至每个函数的作用.
毕竟 知其然不知其所以然 的学习很容易让人一知半解, 灰心丧气. 我不否认我们也有一些非常优秀的开发者, 但是有些时候看看那些 Github 上的歪果开发者的 高星 项目, 何尝不是一种悲哀呢.
末尾复读机
作为闰更作者, 打算来个 Triple Kill 也是付出了老大的勇气的.
如果确实对文中的代码存疑或者确实需要帮助, 欢迎邮箱联系 nvmjs#soxos.me, 除了伸手要全套服务的, 我一定给到力所能及的帮助.
本文采用 CC BY-NC-SA 4.0 协议进行许可
原文地址: soxos.m2d.in/go/tzYAA. 首发于 nvmjs.com 且已经获得原作者许可.
网友评论