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

时间:2025-09-04 15:30:01来源:互联网

下面小编就为大家分享一篇云游戏技术之高速截屏和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 的。代码被大大简化,只保留了核心骨架:

// 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() 方法会按顺序调用一系列私有的初始化函数:

// 文件: 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()

// 文件: main.cpp (DemoApplication::Capture)

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

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

第二步: Preproc()

// 文件: 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()

// 文件: 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() 会被自动调用。

// 文件: main.cpp (DemoApplication::~DemoApplication)

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

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

// 文件: 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 这样一个生命周期,它确保了整个流水线有序、高效地运作。
本站部分内容转载自互联网,如果有网站内容侵犯了您的权益,可直接联系我们删除,感谢支持!