[授权原创][IDD][本站难得一见的正经文] Chapter 2: 参上! 年轻人的第一台虚拟现实器

开局一目录

我大致的将 IDD 的实现按照功能分为下面的三个章节

  1. 初始化一个没有功能的 IDD 壳子.
  2. [本文] 通过 IDD 来创建显示器.
  3. 获取显示器的输出内容与驱动的加载.

你可以根据当前对 IDD 的了解情况来选择性的进行阅读.

课前预习

本着以人为本的原则, 也为了避免半途而废带来的挫败感, 在开始阅读前, 我准备了一份简单的条件自测, 如果您满足这些条件, 那么恭喜你, 踩过这个坑对你来说可能不是难事, 如果你并不具备这些条件但是自诩智慧过人, 那么我相信硬着头皮看完之后可能就能 Get 到这些能力了.

  1. 使用 VC++ 完成一些中等复杂程度的应用开发.
  2. 听说过 DirectX 并且知道他是干啥的.
  3. 打开过 Windows "设备管理器", 知道如何通过 "设备管理器" 安装/更换设备驱动.
  4. 了解过 WDK 甚至用过 WDK 写一个啥功能都没的无聊驱动.
  5. 把电脑搞蓝屏过, 知道如何通过安全模式修好驱动导致的蓝屏.
  6. 知道 Microsoft(微软) 别名 Hugehard(巨硬)
  7. 基础的英文阅读能力, 或者掌握百度翻译的入门级使用.

书接上回

上回我们已经可以完成显示基础的设备初始化, 本章将会开始手把手教你制造年轻人的第一台虚拟现实器.

graph LR
创建WDF驱动-->创建WDF设备-->电源初始化:上电-->ID((初始化显示设备))-->初始化SwapChain

与人方便, 与己偷懒.

在上一章节中, 我们来到了 Monitor::Initialize 的门口准备开始初始化我们的显示设备, 其实实际显示设备的初始化并不是一个简单的 函数调用 过程, 其中涉及到了上一章中在 创建 WDF 设备 时提供的几个回调事件, 在此我们再贴一次部分本章中需要用的几个关键回调注册伪代码方便后续阅读:

    iddFunctions.EvtIddCxParseMonitorDescription = Monitor::ParseDescription;
    iddFunctions.EvtIddCxMonitorGetDefaultDescriptionModes = Monitor::GetDefaultModes;
    iddFunctions.EvtIddCxMonitorQueryTargetModes = Monitor::QueryModes;
    iddFunctions.EvtIddCxMonitorAssignSwapChain = Monitor::AssignSwapChain;
    iddFunctions.EvtIddCxMonitorUnassignSwapChain = Monitor::UnassignSwapChain;

0. 知识点记住了! 初始化显示设备

如同字面意思一般, Monitor::Initialize 中必须要初始化那么些个基础但又没用的信息, 如果要做类比的话, 这个过程类似于创建一个 上电前的 IDD 设备.

我们先看一份伪代码来了解一下大致需要的信息:

Monitor::Initialize(monitorIndex, adapter) {
  
  monitorInformation.MonitorType = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
    monitorInformation.ConnectorIndex = monitorIndex;
  
    monitorInformation.MonitorDescription.Type = IDDCX_MONITOR_DESCRIPTION_TYPE_EDID;
  
  monitorInformation.MonitorDescription.DataSize = sizeof(EDID) || 0;
  monitorInformation.MonitorDescription.pData = EDID || NULL;
  
  monitorInformation.MonitorContainerId = GUID();
  
  monitorCreation.pMonitorInfo = monitorInformation;
  
  createdMonitor = IddCxMonitorCreate(adapter, monitorCreation);
  
  monitorArrived = IddCxMonitorArrival(createdMonitor.MonitorObject);
}

小朋友是不是有很多问号, 为什么没有设置显示器分辨率刷新率这些个耳熟能详参数的位置呢? 关于这个问题, 我们先暂且埋葬自己的好奇, 往下看.

ConnectorIndex 是一个很重要但是也很次要的参数, 系统通过他来保存用户对显示器的设置, 比如: 显示器排序, 采用的分辨率. 但是这个值的变动并不会造成系统的问题, 最多给用户带来一些体验上的麻烦, 比如: 重新开机后显示器的顺序需要重新调整, 分辨率出现错误 等等.

至于 MonitorType 则代表了显示设备的连接接口, 我强烈推荐你设置为我提供的参考值, 没有为什么, 只是能正常工作, 当然了你也可以设置成其他的选项, 区别无非是能否正常工作.

在余下的这么个简短的代码片段中, 凭借着多年练就的火眼金睛, 你应该一眼就能看到出境次数有些多的 EDID.

EDIDExtended Display Identification Data, 一言蔽之就是一个用来描述显示器信息的数据结构, 在此我们不做展开, 因为对于我们的虚拟现实器而言, 这个信息可以直接忽略, 不过这会影响到接下来的剧情路线.

如果你千辛万苦构建出了自己的 EDID 信息, 那么系统将会在 IddCxMonitorCreate 方法之后调用在上一篇中提到并被我贴心附在上一节中的回调方法 EvtIddCxParseMonitorDescription(Monitor::ParseDescription). 我们需要在这个方法中进行 EDID 的解析并将解析出来的结果告知系统. 由于涉及到 EDID 数据结构, 有兴趣的小朋友可以自己去百度一下.

我们的例子中将 monitorInformation.MonitorDescription.pData 简单的设置为 NULL 并将 DataSize 设置为 0 就会使得系统使用另一种获取显示器信息的方法 EvtIddCxMonitorGetDefaultDescriptionModes(Monitor::GetDefaultModes).

之后的例子中我们将以 EvtIddCxMonitorGetDefaultDescriptionModes(Monitor::GetDefaultModes) 作为唯一的回调方法来继续. 毕竟虽然数据的来源不同, 但是要设置的信息都是一样的.

有一说一的提一嘴: 不管你是否启用 EDID, 上面两个回调方法都是需要注册的哟

在完成了上面的这些个一揽子工作后, 我们就可以将显示器带上流水线了, 通过 IddCxMonitorArrival, 显示器就能正常的开始显示内容了.

1. 醒醒! 先处理显示器配置

看完上一张是不是产生了一种一看就会的错觉? 可别忘了 1 分钟前说的 Monitor::GetDefaultModes 回调.

我为 Monitor::GetDefaultModes 准备了一份参考的伪代码实现:

Monitor::GetDefaultModes(monitorObject, pInArgs, pOutArgs) {
  
  monitorMode.totalSize.cx = monitorMode.activeSize.cx = WIDTH;
  monitorMode.totalSize.cy = monitorMode.activeSize.cy = HEIGHT;

  monitorMode.AdditionalSignalInfo.vSyncFreqDivider = 0;
  monitorMode.AdditionalSignalInfo.videoStandard = 255;

  monitorMode.vSyncFreq.Numerator = RATE;
  monitorMode.vSyncFreq.Denominator = 1;
  monitorMode.hSyncFreq.Numerator = RATE * HEIGHT;
  monitorMode.hSyncFreq.Denominator = 1;

  monitorMode.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE;

  monitorMode.pixelRate = RATE * WIDTH * HEIGHT;

  pInArgs->pDefaultMonitorModes[0] = monitorMode;
  pOutArgs->DefaultMonitorModeBufferOutputCount = 1;
  pOutArgs->PreferredMonitorModeIdx = 0;
}

晕了不? 关于 monitorMode 的那些取值规则字段 BALABALA, 微软官方提供了一份非常完整的文档, 我就不班门弄斧了, 将链接贴在这方便大家取阅 点我点我.

我们重点说说文档没的吧. WIDTH / HEIGHT / RATE 分别代表了 水平分辨率 / 垂直分辨率 / 刷新率, 这些个都是大路货, 大家都知道.

由于在我们的例子中只提供了 1 种可用分辨率, 所以我们将 DefaultMonitorModeBufferOutputCount 设置为了 1 而默认使用的分辨率模式 PreferredMonitorModeIdx 则指向了 0.

显示设备的分辨率就此完成!

2. 才怪嘞! 被遗忘的那个方法

回头看看本文开头的与人方便, 除了字面意思很明显的 SwapChain 被拖更到了下一章节, 是不是有啥不对劲?

EvtIddCxMonitorQueryTargetModes(Monitor::QueryModes) 好像没用啊?

怎么可能! Monitor::QueryModes 才是分辨率中的爸爸, 没有他万物皆虚空.

Monitor::QueryModes 提供了 WDF 设备层面上能支持的分辨率模式, 简单的说

Monitor::GetDefaultModes 提供的分辨率必须是 Monitor::QueryModes 的子集.

纳尼, 那么重要的为什么不早说? 因为说早了也没用啊.

其实光看伪代码, 两者还真的没啥大差异:

Monitor::QueryModes(monitorObject, pInArgs, pOutArgs) {
  
  pInArgs->pTargetModes[0].totalSize.cx = pInArgs->pTargetModes[0].activeSize.cx = WIDTH;
  pInArgs->pTargetModes[0].totalSize.cy = pInArgs->pTargetModes[0].activeSize.cy = HEIGHT;

  pInArgs->pTargetModes[0].AdditionalSignalInfo.vSyncFreqDivider = 1;
  pInArgs->pTargetModes[0].AdditionalSignalInfo.videoStandard = 255;

  pInArgs->pTargetModes[0].vSyncFreq.Numerator = RATE;
  pInArgs->pTargetModes[0].vSyncFreq.Denominator = 1;
  pInArgs->pTargetModes[0].hSyncFreq.Numerator = RATE * HEIGHT;
  pInArgs->pTargetModes[0].hSyncFreq.Denominator = 1;

  pInArgs->pTargetModes[0].scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE;

  pInArgs->pTargetModes[0].pixelRate = RATE * WIDTH * HEIGHT;
  
  pInArgs->TargetModeBufferInputCount = 1;
  pOutArgs->TargetModeBufferOutputCount = 1;
  
}

定眼一看, 这不就是一个数组里面塞上一个 monitorMode 吗, 居然那么简单?

还真是. 我们依然在例子中只做了一个分辨率作为示例, 多个分辨率只要向下叠加即可.

重点在于 IddCxAdapterInitAsync, 明眼人一看就知道, 这是一个异步方法, 异步方法就一定有回调, 那么回调是哪个呢?

显然帮助你们踩坑的我一直提前知道了答案, 那就是 EvtIddCxAdapterInitFinished, 对应着我们的 Device::Ready.

其实也好理解, 来电了, 设备初始化了, 下一步就是设备就绪了.

3. 只剩一步了? 对!

如果你有驱动开发经验. 如果你会自己创建一个设备进行驱动挂载测试, 至此你应该已经可以看到一个可用的设备了, 可以说如果你只是想要创建一个虚拟的显示器, 那么你的目的已经完成了.

回看开篇注册的那些回调函数, 目前我们只剩下 SwapChain 的两个函数了, 手把手创建的过程应该就此告一段落.

末了抛个砖: SwapChain 其实才是最多变的部分. 如果说设备的创建几乎都是按部就班, 那么 SwapChain 才是真正需要切合业务实际的部分, 下一章节会很难, 官方示例中也没有包含更多的 SwapChain 处理, 但是如果您真的需要创建一个可用的虚拟显示屏, 继续下一章的阅读一定会对你颇有裨益.

末尾复读机

作为闰更作者, 打算来个 Triple Kill 也是付出了老大的勇气的.

如果确实对文中的代码存疑或者确实需要帮助, 欢迎邮箱联系 nvmjs#soxos.me, 除了伸手要全套服务的, 我一定给到力所能及的帮助.

本文采用 CC BY-NC-SA 4.0 协议进行许可
原文地址: soxos.m2d.in/go/tzYAA. 首发于 nvmjs.com 且已经获得原作者许可.

你可能感兴趣的:([授权原创][IDD][本站难得一见的正经文] Chapter 2: 参上! 年轻人的第一台虚拟现实器)