在上一章 应用程序主控 (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 找到主显示器并创建复制会话,以及如何获取和释放每一帧。