在上一章 应用程序主控 (DemoApplication) 中,我们认识了整个项目的"导演"------DemoApplication 类。
现在,是时候认识DDAImpl 类。
DDAImpl 是什么?
DDAImpl 是我们流水线的第一站:捕获。它的唯一任务,就是从你的电脑屏幕上抓取一帧画面。
想象一下,你想用相机拍一张照片。最简单的方法是拿起手机,对着屏幕拍一张。但这样做的效率低,而且画质会受影响。有没有一种方法能直接从屏幕的"数据源"里把图像"复制"出来呢?
Windows 系统提供了一种名为 桌面复制 API (Desktop Duplication API, DDA) 的高级技术,它正是为此而生。这种技术允许程序直接从显卡的内存中复制屏幕图像,速度极快,效率极高,而且不会有任何画质损失。
DDAImpl 类就是对这个复杂 Windows API 的一个简化封装。它将所有底层的、繁琐的设置和调用都隐藏了起来,只向我们暴露了几个简单易用的接口。
| 真实世界类比 | nvEncDXGI 组件 |
作用 |
|---|---|---|
| 高速摄像机 | DDAImpl |
精确地对准屏幕,不断地"拍照",将每一张照片(屏幕帧)实时传送出去。 |
DDAImpl 的工作成果,是一张存储在显存中的原始图像,我们称之为"图形纹理 (Texture)"。这张纹理随后会被传递给流水线的下一个环节。
如何使用 DDAImpl?
在我们的项目中,DemoApplication(导演)是唯一需要直接和 DDAImpl(摄影师)打交道的人。它的使用流程非常简单,分为两步:
1. 初始化:
在录制开始前,"导演"需要先确保"摄影师"已经准备就绪。这通过调用 DDAImpl 的 Init() 方法来完成。
cpp
// 在 DemoApplication::Init() 内部
// ...
// 创建 DDAImpl 对象
pDDAWrapper = new DDAImpl(pD3DDev, pCtx);
// 初始化 DDAImpl,让它准备好捕获屏幕
hr = pDDAWrapper->Init();
// ...
这段代码告诉 DDAImpl:"嘿,准备开工了!找到主显示器,设置好所有需要的东西。" 如果 Init() 成功返回,就意味着我们的高速摄像机已经架设完毕,对准了屏幕,随时可以开始拍摄。
2. 捕获帧:
当一切准备就绪,在主录制循环中,"导演"会在每一轮都向"摄影师"发出指令:"拍一张!" 这个指令就是调用 GetCapturedFrame() 方法。
cpp
// 在 DemoApplication::Capture() 内部
// 从 DDAImpl 获取一帧画面,并将其存入 pTex2D
// wait 参数告诉它最多等待 500 毫秒来获取新画面
HRESULT hr = pDDAWrapper->GetCapturedFrame(&pTex2D, 500);
这个函数是 DDAImpl 的核心。它会尝试从屏幕上获取一个新的画面。
- 输入参数 :
wait值(单位是毫秒)告诉它,如果没有新画面,应该等待多久。如果在这段时间内屏幕内容有更新,它就会立刻返回。 - 输出 : 如果成功捕获到画面,它会通过第一个参数返回一个
ID3D11Texture2D对象。你可以把它想象成一张存储在显存里的"数字底片",包含了刚刚捕获到的屏幕图像(通常是 RGBA 格式)。
就是这么简单!Init() 一次,然后在循环里不断调用 GetCapturedFrame()。DDAImpl 就能源源不断地为我们的流水线提供最新鲜的屏幕画面。
深入内部:DDAImpl 是如何工作的?
现在,让我们揭开这位"摄影师"的神秘面纱,看看它是如何与 Windows 系统底层交互来完成屏幕捕获的。
初始化 (Init) 的幕后故事
当你调用 Init() 时,DDAImpl 就像一个侦探,需要按图索骥,找到它需要的目标------主显示器的输出信号。
这个过程在代码中(位于 DDAImpl.cpp 的 Init 函数)体现为一系列的查询和调用:
-
找到显卡 (Adapter) 我们从已有的 D3D11 设备出发,层层向上追溯,直到找到代表物理显卡的
IDXGIAdapter对象。cpp// 文件: DDAImpl.cpp (Init) // 从 D3D11 设备获取 DXGI 设备接口 hr = pD3DDev->QueryInterface(__uuidof(IDXGIDevice2), (void**)&pDevice); // ... // 从 DXGI 设备获取它的"父亲"------显卡适配器 hr = pDevice->GetParent(__uuidof(IDXGIAdapter), (void**)&pAdapter); // ... -
找到主显示器 (Output) 一块显卡可能连接了多个显示器。我们通过
EnumOutputs(0, ...)来获取索引为 0 的显示器,它通常是主显示器。cpp// 文件: DDAImpl.cpp (Init) // 枚举并获取索引为 0 的显示器 hr = pAdapter->EnumOutputs(0, &pOutput); // ... -
创建复制会话 (Duplication) 这是最关键的一步。我们请求显示器对象为我们创建一个"复制品"。
cpp// 文件: DDAImpl.cpp (Init) // 将 pOutput 转换为支持复制功能的 IDXGIOutput1 接口 hr = pOutput->QueryInterface(__uuidof(IDXGIOutput1), (void**)&pOut1); // ... // 请求创建输出复制接口,结果保存在 pDup 成员变量中 hr = pOut1->DuplicateOutput(pDevice, &pDup);执行完这句代码后,
pDup(类型为IDXGIOutputDuplication*)就成了我们与桌面复制功能交互的唯一句柄。
获取帧 (GetCapturedFrame) 的秘密
当 Init() 完成后,我们就可以通过 pDup 这个"钥匙"来不断地获取新画面了。GetCapturedFrame() 的工作流程如下:
-
释放上一帧资源 (如果有的话):每次获取新帧之前,必须先告诉系统:"上一帧我已经用完了,你可以回收了。"
cpp// 文件: DDAImpl.cpp (GetCapturedFrame) // 如果 pResource 不为空,说明还持有着上一帧的资源 if (pResource) { pDup->ReleaseFrame(); // 告诉 DDA 可以释放了 pResource->Release(); // 释放我们自己的引用 pResource = nullptr; } -
请求下一帧 : 调用
AcquireNextFrame来获取最新的屏幕画面。这个调用会"阻塞"程序,直到有新画面出现或者等待超时。cpp// 文件: DDAImpl.cpp (GetCapturedFrame) DXGI_OUTDUPL_FRAME_INFO frameInfo; // 用来存储帧信息 // 等待最多 wait 毫秒,获取下一帧 hr = pDup->AcquireNextFrame(wait, &frameInfo, &pResource); if (FAILED(hr)) { // 如果是超时,这是正常情况,直接返回错误码让上层处理 if (hr == DXGI_ERROR_WAIT_TIMEOUT) { /* ... */ } return hr; }如果成功,
pResource会指向一个通用的IDXGIResource对象,它代表了新的屏幕帧。frameInfo则包含了这帧的详细信息,比如时间戳。 -
转换为纹理 :
IDXGIResource是一个比较通用的类型,我们需要把它转换成 D3D11 能直接使用的ID3D11Texture2D格式。cpp// 文件: DDAImpl.cpp (GetCapturedFrame) // 将通用的资源对象"查询"并转换为我们需要的 2D 纹理对象 hr = pResource->QueryInterface(__uuidof(ID3D11Texture2D), (void**)ppTex2D); return hr;完成这一步后,
ppTex2D指向的指针就包含了这张宝贵的屏幕截图,可以交给流水线的下一个环节了。
总结
在本章中,我们深入了解了流水线的起点------DDAImpl。
- 我们知道了
DDAImpl是对 Windows 桌面复制 API 的一个封装,是实现高性能屏幕捕获的关键。 - 它的角色是流水线中的"高速摄像机",负责抓取原始的屏幕图像。
- 我们学习了它的核心用法:通过
Init()进行初始化,然后循环调用GetCapturedFrame()来获取一帧帧的画面(ID3D11Texture2D格式)。 - 我们还探究了其内部原理,了解了它如何通过 DXGI 找到主显示器并创建复制会话,以及如何获取和释放每一帧。