云游戏技术之高速截屏和GPU硬编码 (2) 应用程序主控

在上一章 捕获-预处理-编码流水线] 中,我们了解了整个屏幕录制过程就像一条工厂流水线。数据从捕获开始,经过预处理,最后被编码成视频。

那么,问题来了:谁是这条流水线的"总管"呢?谁来确保每个环节都按时、按顺序地工作?

答案就是我们本章的主角:DemoApplication 类。

什么是 DemoApplication

DemoApplication 是整个项目的"大脑"和"总指挥"。它是一个 C++ 类,专门被设计用来封装和管理流水线中的所有核心组件。它负责:

  1. 创建和初始化:在程序开始时,它会创建捕获器、预处理器和编码器这三个核心组件,并确保它们都已准备就绪。
  2. 驱动流水线:在录制过程中,它在一个循环里发号施令,让每个组件依次完成自己的工作:捕获一帧、处理该帧、编码该帧。
  3. 资源清理:在程序结束时,它负责安全地释放所有组件占用的资源,避免内存泄漏。

我们可以再次使用上一章的电影制作 比喻来理解它。如果说 DDAImpl 是摄影师,RGBToNV12 是后期调色师,NvEncoderD3D11 是剪辑师,那么 DemoApplication 就是 电影导演

这位导演的工作就是:

  • 前期筹备 (Init):招募并组织好整个剧组(摄影、调色、剪辑团队)。
  • 开机拍摄 (循环执行) :对每一幕(每一帧画面)下达指令:"摄影师,开拍!" (Capture),"调色师,处理一下素材!" (Preproc),"剪辑师,把这段加到电影里!" (Encode)。
  • 杀青散场 (Cleanup):电影拍完后,解散剧组,收拾片场。

通过将所有复杂的协调工作都封装在 DemoApplication 内部,我们的主程序 (main.cpp 里的 Grab60FPS 函数) 逻辑变得异常清晰和简单。

DemoApplication 如何简化工作流程

让我们看看 main.cpp 中的 Grab60FPS 函数是如何使用 DemoApplication 的。代码被大大简化,只保留了核心骨架:

cpp 复制代码
// Demo 60 FPS (approx.) capture
int Grab60FPS(int nFrames)
{
    // 1. 创建我们的"导演"
    DemoApplication Demo;

    // 2. 导演进行前期筹备,初始化所有组件
    HRESULT hr = Demo.Init();
    if (FAILED(hr))
    {
        // 如果初始化失败,就直接退出
        return -1;
    }

    // 3. 开始循环拍摄每一帧
    do
    {
        // "导演"指挥流水线工作
        hr = Demo.Capture(wait);  // 捕获
        hr = Demo.Preproc();     // 预处理
        hr = Demo.Encode();      // 编码
        capturedFrames++;
    } while (capturedFrames <= nFrames);

    return 0; // 拍摄完成,程序结束
}

看到了吗?主逻辑就是简单地创建一个 Demo 对象,调用 Init(),然后在循环里调用 Capture()Preproc()Encode()。所有复杂的细节都被隐藏在了 DemoApplication 类的内部。这就是封装带来的巨大好处!

深入内部:DemoApplication 的三大职责

现在,让我们揭开导演的神秘面纱,看看它的内部是如何运作的。DemoApplication 的工作可以分为三个主要阶段:初始化、执行循环和清理。

1. 初始化阶段 (Init 方法)

当你调用 Demo.Init() 时,"导演"开始了他的前期筹备工作。他需要确保所有的"工作人员"------也就是流水线上的各个组件------都已就位。

Init() 方法会按顺序调用一系列私有的初始化函数:

cpp 复制代码
// 文件: main.cpp (DemoApplication::Init)

HRESULT Init()
{
    HRESULT hr = S_OK;

    // 步骤 1: 初始化 DirectX 环境,这是所有图形操作的基础
    hr = InitDXGI();
    returnIfError(hr);

    // 步骤 2: 初始化桌面捕获器
    hr = InitDup();
    returnIfError(hr);

    // 步骤 3: 初始化 NVENC 编码器
    hr = InitEnc();
    returnIfError(hr);

    // 步骤 4: 初始化色彩转换器
    hr = InitColorConv();
    returnIfError(hr);

    // ... 其他初始化 ...
    return hr;
}

这个过程就像一个清单,DemoApplication 会逐一确认:

  1. InitDXGI(): 搭建好"片场"(DirectX 11 环境)。
  2. InitDup(): 确保"摄影师" 桌面复制接口 (DDAImpl) 已准备就绪。
  3. InitEnc(): 确保"剪辑师" NVENC 硬件编码器封装 (NvEncoderD3D11) 已准备就绪。
  4. InitColorConv(): 确保"调色师" 色彩空间转换器 (RGBToNV12) 已准备就绪。

我们可以用一个时序图来更清晰地展示这个过程:

sequenceDiagram participant Main as Grab60FPS 函数 participant App as DemoApplication participant DDA as DDAImpl participant Encoder as NvEncoderD3D11 participant Converter as RGBToNV12 Main->>App: Init() App->>App: InitDXGI() (准备DX11环境) App->>DDA: new DDAImpl() & Init() DDA-->>App: 初始化完成 App->>Encoder: new NvEncoderD3D11() & CreateEncoder() Encoder-->>App: 初始化完成 App->>Converter: new RGBToNV12() & Init() Converter-->>App: 初始化完成 App-->>Main: 初始化成功

只有当所有组件都报告"准备就绪"后,Init() 才会成功返回,录制工作才能正式开始。

2. 执行循环 (Capture, Preproc, Encode)

初始化完成后,程序进入 do-while 循环,开始一帧一帧地处理画面。DemoApplication 在这里扮演着数据传递和流程控制的核心角色。

数据是如何在组件间流动的?

DemoApplication 内部维护着几个重要的"中转站",它们是 DirectX 纹理(可以理解为显存中的一块图像区域):

  • pDupTex2D: 用于存放从屏幕上捕获的原始 RGBA 图像
  • pEncBuf: 用于存放经过色彩转换后的NV12 格式图像

现在我们来看看三步曲是如何协作的:

第一步: Capture()

cpp 复制代码
// 文件: main.cpp (DemoApplication::Capture)

HRESULT Capture(int wait)
{
    // 调用 DDAImpl 组件,将捕获到的帧存入 pDupTex2D
    HRESULT hr = pDDAWrapper->GetCapturedFrame(&pDupTex2D, wait); 
    // ... 错误处理 ...
    return hr;
}

导演喊"开拍!",pDDAWrapper(桌面复制接口 (DDAImpl) 对象)立即工作,它将屏幕画面捕获到 pDupTex2D 这个纹理中。现在,我们有了一帧原始的 RGBA 格式的画面。

第二步: Preproc()

cpp 复制代码
// 文件: main.cpp (DemoApplication::Preproc)

HRESULT Preproc()
{
    // 从编码器获取一个用于存放 NV12 图像的空纹理
    const NvEncInputFrame *pEncInput = pEnc->GetNextInputFrame();
    pEncBuf = (ID3D11Texture2D *)pEncInput->inputPtr;

    // 调用色彩转换器,将 pDupTex2D 的内容转成 NV12 并存入 pEncBuf
    hr = pColorConv->Convert(pDupTex2D, pEncBuf);

    // 原始图像已经没用了,释放它
    SAFE_RELEASE(pDupTex2D);
    return hr;
}

导演把原始素材(pDupTex2D)交给调色师 pColorConv(色彩空间转换器 (RGBToNV12) 对象)。调色师将其转换为编码器最喜欢的 NV12 格式,并把结果存放在 pEncBuf 中。

第三步: Encode()

cpp 复制代码
// 文件: main.cpp (DemoApplication::Encode)

HRESULT Encode()
{
    // ...
    // 将预处理好的 NV12 图像 (pEncBuf) 交给编码器
    pEnc->EncodeFrame(vPacket);
    // 将编码后的数据写入文件
    WriteEncOutput();
    // ...
    // NV12 图像也用完了,释放它
    SAFE_RELEASE(pEncBuf);
    return hr;
}

最后,导演将处理好的 NV12 素材(pEncBuf)交给剪辑师 pEnc(NVENC 硬件编码器封装 (NvEncoderD3D11) 对象)。剪辑师利用 GPU 硬件加速,将其压缩成 H.264 视频数据,然后 DemoApplication 负责将这些数据写入到最终的视频文件中。

至此,一帧画面的完整处理流程就结束了。这个循环会不断重复,直到录制到指定数量的帧。

3. 清理阶段 (Cleanup 和析构函数)

当录制结束,DemoApplication 对象生命周期结束时(在 Grab60FPS 函数末尾),它的析构函数 ~DemoApplication() 会被自动调用。

cpp 复制代码
// 文件: main.cpp (DemoApplication::~DemoApplication)

~DemoApplication()
{
    // 调用 Cleanup 方法来释放所有资源
    Cleanup(true); 
}

析构函数会调用 Cleanup() 方法,这是一个专门负责"打扫战场"的函数。它会按照与初始化相反的顺序,安全地释放所有组件和资源。

cpp 复制代码
// 文件: main.cpp (DemoApplication::Cleanup)

void Cleanup(bool bDelete = true)
{
    // 释放 DDA 组件
    if (pDDAWrapper)
    {
        pDDAWrapper->Cleanup();
        delete pDDAWrapper;
        pDDAWrapper = nullptr;
    }

    // 释放编码器,并确保所有缓冲的帧都被写入文件
    if (pEnc)
    {
        pEnc->EndEncode(vPacket);
        WriteEncOutput();
        pEnc->DestroyEncoder();
        delete pEnc;
        pEnc = nullptr;
    }
    
    // ... 释放其他资源,如色彩转换器、DXGI 设备等 ...
}

这确保了程序退出时不会有任何资源泄漏,就像电影拍完后,所有工作人员都能拿到工资,所有设备都归还妥当一样。

总结

在本章中,我们认识了 nvEncDXGI 项目的总指挥------DemoApplication 类。

  • 它是"导演" :它不亲自执行具体任务,而是负责协调和管理手下的专业团队(DDAImpl, RGBToNV12, NvEncoderD3D11)。
  • 它封装了复杂性:通过将初始化、执行循环和资源清理的逻辑都封装起来,它让主程序的代码变得极其简洁。
  • 它定义了清晰的流程 :通过 Init -> Capture -> Preproc -> Encode -> Cleanup 这样一个生命周期,它确保了整个流水线有序、高效地运作。
相关推荐
沐怡旸8 小时前
【C++基础知识】深入剖析C和C++在内存分配上的区别
c++
studytosky8 小时前
C语言数据结构之双向链表
c语言·数据结构·c++·算法·链表·c
沐怡旸8 小时前
【底层机制】malloc 在实现时为什么要对大小内存采取不同策略?
c++
HABuo9 小时前
【C++进阶篇】学习C++就看这篇--->多态超详解
c语言·开发语言·c++·后端·学习
1白天的黑夜19 小时前
哈希表-1.两数之和-力扣(LeetCode)
c++·leetcode·哈希表
哼?~10 小时前
list模拟实现
开发语言·c++
春花秋月夏海冬雪10 小时前
代码随想录刷题Day47
c++·平衡二叉树的构建·二叉树中序遍历·代码随想录刷题
七牛云行业应用10 小时前
私有化存储架构演进:从传统NAS到一体化数据平台
大数据·人工智能·架构·云计算·七牛云存储
深耕AI11 小时前
【MFC应用创建后核心文件详解】项目名.cpp、项目名.h、项目名Dlg.cpp 和 项目名Dlg.h 的区别与作用
c++·mfc