在上一章 桌面复制接口 (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. 初始化:
在录制开始前,导演需要设置好这台"视频压缩机"。这通过 DemoApplication
的 InitEnc()
方法完成。
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()
时,内部发生了什么。
进行高速压缩 GPU-->>Encoder: 编码完成,输出数据已放入输出缓冲区 Encoder->>GPU: 拷贝输出数据到 CPU 内存 GPU-->>Encoder: 数据已拷贝 Encoder-->>App: 返回包含 H.264 数据的 vPacket
这个流程的关键在于异步处理 和零拷贝:
- 获取输入缓冲区 :
DemoApplication
并不是自己创建用于编码的图像内存,而是向NvEncoderD3D11
"借"一个。这通过GetNextInputFrame()
实现。这确保了图像数据从始至终都保留在显存中,避免了从内存到显存的低效拷贝。 - 提交编码任务 : 当
EncodeFrame()
被调用时,NvEncoderD3D11
只是向 GPU 驱动提交了一个"编码请求",然后就可以立刻返回,让 CPU 去做别的事情。GPU 的 NVENC 硬件会在后台独立完成繁重的压缩工作。 - 获取输出 : 当 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。