云游戏技术之高速截屏和GPU硬编码 (3) 桌面复制接口 (Desktop Duplication API)

在上一章 应用程序主控 (DemoApplication) 中,我们认识了整个项目的"导演"------DemoApplication 类。

现在,是时候认识DDAImpl 类。

DDAImpl 是什么?

DDAImpl 是我们流水线的第一站:捕获。它的唯一任务,就是从你的电脑屏幕上抓取一帧画面。

想象一下,你想用相机拍一张照片。最简单的方法是拿起手机,对着屏幕拍一张。但这样做的效率低,而且画质会受影响。有没有一种方法能直接从屏幕的"数据源"里把图像"复制"出来呢?

Windows 系统提供了一种名为 桌面复制 API (Desktop Duplication API, DDA) 的高级技术,它正是为此而生。这种技术允许程序直接从显卡的内存中复制屏幕图像,速度极快,效率极高,而且不会有任何画质损失。

DDAImpl 类就是对这个复杂 Windows API 的一个简化封装。它将所有底层的、繁琐的设置和调用都隐藏了起来,只向我们暴露了几个简单易用的接口。

真实世界类比 nvEncDXGI 组件 作用
高速摄像机 DDAImpl 精确地对准屏幕,不断地"拍照",将每一张照片(屏幕帧)实时传送出去。

DDAImpl 的工作成果,是一张存储在显存中的原始图像,我们称之为"图形纹理 (Texture)"。这张纹理随后会被传递给流水线的下一个环节。

如何使用 DDAImpl

在我们的项目中,DemoApplication(导演)是唯一需要直接和 DDAImpl(摄影师)打交道的人。它的使用流程非常简单,分为两步:

1. 初始化:

在录制开始前,"导演"需要先确保"摄影师"已经准备就绪。这通过调用 DDAImplInit() 方法来完成。

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 就像一个侦探,需要按图索骥,找到它需要的目标------主显示器的输出信号。

sequenceDiagram participant App as DemoApplication participant DDA as DDAImpl participant DXGI as Windows DXGI 系统 participant GPU as 显卡驱动 App->>DDA: Init() DDA->>DXGI: "你好,我想和你谈谈显卡的事" DXGI-->>DDA: "好的,这是显卡适配器(Adapter)对象" DDA->>DXGI: "请问这块显卡连接的第一个显示器(Output 0)是哪个?" DXGI-->>DDA: "就是这个,给你显示器对象" DDA->>DXGI: "太好了!请为这个显示器创建一个'复制会话'(DuplicateOutput)" DXGI->>GPU: "准备一下,开始复制这个显示器的画面" GPU-->>DXGI: "复制会话已建立" DXGI-->>DDA: "给你'复制会话'的钥匙(pDup 对象),以后用它来拿画面" DDA-->>App: 初始化成功!

这个过程在代码中(位于 DDAImpl.cppInit 函数)体现为一系列的查询和调用:

  1. 找到显卡 (Adapter) 我们从已有的 D3D11 设备出发,层层向上追溯,直到找到代表物理显卡的 IDXGIAdapter 对象。

    cpp 复制代码
    // 文件: DDAImpl.cpp (Init)
    
    // 从 D3D11 设备获取 DXGI 设备接口
    hr = pD3DDev->QueryInterface(__uuidof(IDXGIDevice2), (void**)&pDevice);
    // ...
    // 从 DXGI 设备获取它的"父亲"------显卡适配器
    hr = pDevice->GetParent(__uuidof(IDXGIAdapter), (void**)&pAdapter);
    // ...
  2. 找到主显示器 (Output) 一块显卡可能连接了多个显示器。我们通过 EnumOutputs(0, ...) 来获取索引为 0 的显示器,它通常是主显示器。

    cpp 复制代码
    // 文件: DDAImpl.cpp (Init)
    
    // 枚举并获取索引为 0 的显示器
    hr = pAdapter->EnumOutputs(0, &pOutput);
    // ...
  3. 创建复制会话 (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() 的工作流程如下:

  1. 释放上一帧资源 (如果有的话):每次获取新帧之前,必须先告诉系统:"上一帧我已经用完了,你可以回收了。"

    cpp 复制代码
    // 文件: DDAImpl.cpp (GetCapturedFrame)
    
    // 如果 pResource 不为空,说明还持有着上一帧的资源
    if (pResource)
    {
        pDup->ReleaseFrame(); // 告诉 DDA 可以释放了
        pResource->Release(); // 释放我们自己的引用
        pResource = nullptr;
    }
  2. 请求下一帧 : 调用 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 则包含了这帧的详细信息,比如时间戳。

  3. 转换为纹理 : 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 找到主显示器并创建复制会话,以及如何获取和释放每一帧。
相关推荐
hy____1233 小时前
C++异常
开发语言·c++
kyle~4 小时前
海康摄像头开发---标准配置结构体(NET_DVR_STD_CONFIG)
运维·服务器·c++·算法·microsoft·海康威视
睡不醒的kun4 小时前
leetcode算法刷题的第二十四天
数据结构·c++·算法·leetcode·职场和发展·贪心算法
君鼎4 小时前
More Effective C++ 条款23:考虑使用其他程序库
c++
Q741_1474 小时前
C++ 面试高频考点 力扣 852. 山脉数组的峰顶索引 二分查找 题解 每日一题
c++·算法·leetcode·面试·二分查找
CYRUS_STUDIO5 小时前
FART 脱壳不再全量!用一份配置文件精准控制节奏与范围
android·c++·逆向
乌萨奇也要立志学C++5 小时前
【C++详解】C++11(三) 可变参数模板、包扩展、empalce系列接⼝、新的类功能
c++
星星火柴9366 小时前
C++“类吸血鬼幸存者”游戏制作的要点学习
c++·笔记·学习·游戏
ajassi20006 小时前
开源 C++ QT Widget 开发(九)图表--仪表盘
linux·c++·qt·开源