云游戏技术之高速截屏和GPU硬编码 (4) NVENC 硬件编码 (NvEncoderD3D11)

在上一章 桌面复制接口 (DDAImpl) 中,我们认识了流水线中的"高速摄影师"。它为我们源源不断地提供了来自屏幕的原始图像"数字底片"。

现在我们面临一个新问题:这些"数字底片"(原始图像帧)非常大。如果我们把它们一张张直接存起来,文件会很快变得巨大无比,根本不适合存储和传输。我们需要一种方法,将这些连续的图片压缩成一个紧凑的视频文件,比如我们常见的 .mp4.h264 文件。

这项艰巨的任务就交给了我们流水线的终点站------NvEncoderD3D11,一位利用 GPU 硬件进行超高速压缩的"专业剪辑师"。

NvEncoderD3D11 是什么?

NvEncoderD3D11 是我们流水线的第三步,也是最后一步:编码 (Encode)

简单来说,它是一个与 NVIDIA GPU 内置的专用视频编码硬件(称为 NVENC)进行沟通的桥梁。传统的视频编码主要依靠 CPU 来完成,这个过程非常消耗计算资源,而且速度较慢。而 NVENC 是专门为视频压缩设计的硬件电路,其效率远非 CPU 可比。

NvEncoderD3D11 这个类直接来自 NVIDIA 官方提供的 Video SDK,它为我们封装了所有与 NVENC 硬件交互的复杂细节。我们只需要把准备好的图像帧"喂"给它,它就能利用 GPU 的强大能力,闪电般地输出压缩好的 H.264 视频数据流。

真实世界类比 nvEncDXGI 组件 作用
专业视频压缩机/剪辑师 NvEncoderD3D11 接收一帧帧处理好的图像,利用 GPU 硬件将其高效压缩成 H.264 视频数据包。

它的工作就像一台专业的视频压缩设备:你把一张张高清图片放进去,它就能快速地输出一个体积小巧的视频文件,方便存储和网络传输。

如何使用 NvEncoderD3D11

DDAImpl 一样,我们的"导演" 应用程序主控 (DemoApplication) 负责与 NvEncoderD3D11 的所有交互。整个过程可以分为三个阶段:

1. 初始化:

在录制开始前,导演需要设置好这台"视频压缩机"。这通过 DemoApplicationInitEnc() 方法完成。

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

// 创建编码器实例,指定设备、输出视频的宽高和输入图像格式
pEnc = new NvEncoderD3D11(pD3DDev, w, h, fmt);

// ... 设置编码参数,比如视频格式、性能预设等 ...
pEnc->CreateDefaultEncoderParams(&encInitParams, NV_ENC_CODEC_H264_GUID, NV_ENC_PRESET_LOW_LATENCY_HP_GUID);

// 根据参数,正式创建编码器会话
pEnc->CreateEncoder(&encInitParams);

这段代码做了几件重要的事情:

  • 创建 NvEncoderD3D11 对象,并告诉它视频的尺寸等基本信息。
  • 配置编码参数(encInitParams),比如我们希望输出 H.264 格式的视频,并且追求"低延迟高性能"(LOW_LATENCY_HP)。
  • 最后调用 CreateEncoder(),让它去和 GPU 硬件沟通,准备好开始工作。

2. 编码一帧:开始压缩

在主录制循环中,每当一帧画面准备好后,导演就会下达"编码!"的指令。这个指令就是调用 EncodeFrame() 方法。

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

// vPacket 是一个容器,用来接收编码后输出的数据
std::vector<std::vector<uint8_t>> vPacket;

// 将准备好的图像帧交给编码器进行压缩
pEnc->EncodeFrame(vPacket);

// 将压缩好的数据包写入文件
WriteEncOutput();

EncodeFrame 是编码工作的核心。它会接收我们准备好的图像帧(注意:它期望的是经过预处理的 NV12 格式图像,我们将在下一章讨论),然后驱动 NVENC 硬件进行压缩。

压缩完成后,它会把结果------一个或多个 H.264 视频数据包(Packet)------填充到 vPacket 这个容器里。我们的程序再把这些数据包写入文件,视频就这样一帧一帧地生成了。

3. 结束编码:收尾工作

当录制结束时,编码器内部可能还缓存着几帧尚未完全输出的数据。我们需要告诉它:"工作结束了,把所有剩下的东西都交出来!"

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

// 刷出(Flush)编码器中所有剩余的帧
pEnc->EndEncode(vPacket);
// 将最后的数据包也写入文件
WriteEncOutput();

// 销毁编码器,释放所有硬件资源
pEnc->DestroyEncoder();
delete pEnc;
pEnc = nullptr;
  • EndEncode() 会清空编码器的内部缓冲区,确保视频的结尾是完整的。
  • DestroyEncoder() 则会通知 GPU 释放所有为这次编码会话分配的资源。这是一个非常重要的步骤,可以防止资源泄漏。

深入内部:NvEncoderD3D11 是如何工作的?

NvEncoderD3D11 的高效来自于它和 GPU 的紧密协作。它在显存中管理着一系列输入和输出缓冲区,整个数据流动过程几乎不涉及慢速的系统内存。

编码一帧的幕后故事

让我们来看一下,当 DemoApplication 调用 Encode() 时,内部发生了什么。

sequenceDiagram participant App as DemoApplication participant Encoder as NvEncoderD3D11 participant GPU as NVIDIA 驱动/硬件 (NVENC) Note over App, Encoder: 在 Preproc() 步骤中... App->>Encoder: GetNextInputFrame() (请给我一个空的输入缓冲区) Encoder-->>App: 返回一个指向显存中纹理的指针 Note over App, Encoder: App 将图像数据复制到该缓冲区... Note over App, Encoder: 现在进入 Encode() 步骤... App->>Encoder: EncodeFrame(输入缓冲区) Encoder->>GPU: 提交编码任务 (包含输入缓冲区指针) Note right of GPU: NVENC 硬件开始异步工作,
进行高速压缩 GPU-->>Encoder: 编码完成,输出数据已放入输出缓冲区 Encoder->>GPU: 拷贝输出数据到 CPU 内存 GPU-->>Encoder: 数据已拷贝 Encoder-->>App: 返回包含 H.264 数据的 vPacket

这个流程的关键在于异步处理零拷贝

  1. 获取输入缓冲区 : DemoApplication 并不是自己创建用于编码的图像内存,而是向 NvEncoderD3D11"借"一个。这通过 GetNextInputFrame() 实现。这确保了图像数据从始至终都保留在显存中,避免了从内存到显存的低效拷贝。
  2. 提交编码任务 : 当 EncodeFrame() 被调用时,NvEncoderD3D11 只是向 GPU 驱动提交了一个"编码请求",然后就可以立刻返回,让 CPU 去做别的事情。GPU 的 NVENC 硬件会在后台独立完成繁重的压缩工作。
  3. 获取输出 : 当 GPU 完成编码后,NvEncoderD3D11 会负责从显存的输出缓冲区中取回压缩好的 H.264 数据,并返回给调用者。

关键代码解析

让我们将上面的流程与代码对应起来。

第一步:在预处理阶段获取输入缓冲区

这个操作实际上发生在 Preproc() 函数中,因为它需要一个目标位置来存放转换后的颜色数据。

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

// 向编码器申请一个可用的输入缓冲区
const NvEncInputFrame *pEncInput = pEnc->GetNextInputFrame();

// 这个缓冲区本质上是一个 D3D11 纹理,我们将其指针保存下来
pEncBuf = (ID3D11Texture2D *)pEncInput->inputPtr;

// 接下来,色彩转换器会把数据写入 pEncBuf
// ...

这一步非常巧妙。我们直接将预处理(色彩转换)的结果写入了编码器指定的显存位置,避免了任何不必要的数据拷贝。

第二步:在编码阶段提交任务

Encode() 函数被调用时,pEncBuf 已经包含了准备好的图像数据。

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

try
{
    // 告诉编码器处理我们刚刚填充好的那个输入缓冲区
    // 编码结果会被放入 vPacket
    pEnc->EncodeFrame(vPacket);

    // 将 vPacket 中的数据写入文件
    WriteEncOutput();
}
catch (...)
{
    // ...
}

EncodeFrame 内部会处理所有与 GPU 的复杂通信,我们只需要等待它返回压缩好的数据即可。

总结

在本章中,我们认识了流水线中负责"临门一脚"的强大组件------NvEncoderD3D11

  • 我们知道了它是利用 NVIDIA NVENC 硬件进行视频编码的封装类,是实现高性能录制的关键。
  • 它的角色是流水线中的"专业视频压缩机",负责将图像帧压缩成 H.264 视频流。
  • 我们学习了它的核心用法:通过 CreateEncoder() 初始化,循环调用 EncodeFrame() 进行编码,最后通过 EndEncode()DestroyEncoder() 完成收尾。
  • 我们还探究了其内部的高效工作机制,理解了它如何通过管理显存中的输入/输出缓冲区,实现与 GPU 的高效异步协作。

至此,我们已经了解了流水线的起点 桌面复制接口 (DDAImpl) 和终点 NvEncoderD3D11。但是,我们留下了一个悬念:DDAImpl 捕获的是 RGBA 格式的图像,而 NvEncoderD3D11 最高效的输入格式是 NV12。

相关推荐
重启的码农3 小时前
云游戏技术之高速截屏和GPU硬编码 (3) 桌面复制接口 (Desktop Duplication API)
c++·云计算·音视频开发
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++·笔记·学习·游戏