在上一章中,我们认识了流水线终点NVENC 硬件编码器封装 (NvEncoderD3D11)。我们了解到,这位剪辑师虽然工作效率极高,但它有一个小小的"偏好":它最喜欢处理一种叫做 NV12 的特殊图像格式。
然而,我们流水线起点的桌面复制接口 (DDAImpl 捕获到的画面却是常见的 RGBA 格式。这就好比摄影师拍了一张色彩斑斓的数码照片(RGBA),而印刷厂的机器(编码器)只认一种特殊的印刷油墨模式(NV12)。
我们该如何解决这个"格式不兼容"的问题呢?
答案就是我们本章的主角,流水线中承上启下的关键一环:RGBToNV12
------一位专业的"色彩空间转换器"。
RGBToNV12
是什么?
RGBToNV12
是我们流水线的第二步:预处理 (Pre-process)。
它的任务非常专一:接收一张 RGBA 格式的图像,并利用显卡内置的视频处理功能,将其快速地转换为 NV12 格式。它就像一位专业的色彩专家,能将一幅色彩丰富的油画(RGB)转换成适合印刷的特定颜色模式(NV12),同时保证视觉效果基本不变。
为什么需要这种转换?
你可能会问,为什么不直接让编码器处理 RGBA 图像呢?
这背后是视频压缩的核心思想。
- RGBA:存储了每个像素完整的红(R)、绿(G)、蓝(B)和透明度(A)信息。这种格式信息完整,但数据量巨大。
- NV12 :是一种 YUV 颜色格式的变体。它将图像信息分为两部分:
- 亮度 (Luma, Y):代表图像的明暗信息,每个像素都有一个独立的亮度值。
- 色度 (Chroma, UV):代表图像的颜色信息。它利用了人眼对亮度比对颜色更敏感的特点,让相邻的几个像素共享一套颜色信息。
通过共享颜色信息,NV12 格式在人眼几乎察觉不到画质损失的情况下,大大减少了数据量。这使得它成为视频编码器的"天选之子",可以实现更高的压缩率。
真实世界类比 | nvEncDXGI 组件 |
作用 |
---|---|---|
色彩翻译官/调色师 | RGBToNV12 |
将"摄影师"拍摄的 RGBA 原始素材,翻译成"剪辑师"最容易理解和处理的 NV12 格式。 |
这个转换过程完全在 GPU 上进行,速度飞快,确保了它不会成为我们高性能录制流水线的瓶颈。
如何使用 RGBToNV12
?
在我们的项目中,应用程序主控 (DemoApplication) 负责调用这位"调色师"来处理每一帧画面。这个过程发生在 Preproc()
方法中。
cpp
// 在 DemoApplication::Preproc() 内部
HRESULT Preproc()
{
// ... 从编码器那里"借"来一个空的 NV12 纹理 pEncBuf ...
// 调用色彩转换器,执行转换!
// 输入: pDupTex2D (包含 RGBA 图像)
// 输入: pEncBuf (空的 NV12 纹理)
hr = pColorConv->Convert(pDupTex2D, pEncBuf);
// ... 释放原始的 RGBA 图像 ...
return hr;
}
使用起来非常直观:
- 我们准备好输入 :一张从
DDAImpl
捕获的、包含 RGBA 图像的纹理 (pDupTex2D
)。 - 我们准备好输出目标 :一张从
NvEncoderD3D11
"借"来的、空的 NV12 格式纹理 (pEncBuf
)。 - 调用
pColorConv->Convert()
方法,它会施展"魔法",将pDupTex2D
的内容转换后,填充到pEncBuf
中。
转换完成后,pEncBuf
就包含了编码器所需要的 NV12 格式图像,可以被送入流水线的下一个环节了。
深入内部:RGBToNV12
是如何工作的?
RGBToNV12
的高效并非源于复杂的 CPU 计算,而是巧妙地利用了 DirectX 中一个强大的功能:视频处理器 (Video Processor)。这是现代显卡普遍具备的硬件功能,专门用于视频播放中的各种图像处理,比如缩放、去隔行,以及我们这里用到的色彩空间转换。
转换的幕后故事
让我们用一个时序图来看看,当 Convert
方法被调用时,RGBToNV12
是如何与 DirectX 沟通,让 GPU 来完成实际工作的。
这个流程可以简化为三步:
- 创建"窗口" :
RGBToNV12
首先为输入的 RGBA 纹理和输出的 NV12 纹理分别创建了"输入视图"和"输出视图"。你可以把"视图(View)"想象成让视频处理器能够"看到"并操作这块显存区域的特殊窗口。 - 配置处理器 :它会确保内部的"视频处理器"(
m_pVP
)已经根据输入输出图像的尺寸等信息配置好了。 - 下达指令 :最后,它调用
VideoProcessorBlt
函数。Blt
是"块图像传输 (Block Image Transfer)"的缩写,你可以把它理解为一条指令:"嘿,GPU!请启动视频处理器,从输入视图读取数据,转换成 NV12 格式,然后把结果写入输出视图。"
关键代码解析
现在,让我们打开 Preproc.cpp
文件,看看 Convert
函数中的关键代码是如何实现上述流程的。
第一步:检查并创建视频处理器 (如果需要)
视频处理器是与特定视频尺寸相关的。如果屏幕分辨率变了,就需要重新创建一个。
cpp
// 文件: Preproc.cpp (Convert)
// 检查输入/输出图像的尺寸是否发生变化
if (m_pVP)
{
if (m_inDesc.Width != inDesc.Width || /*...尺寸不同...*/)
{
// 如果尺寸变了,就释放旧的处理器
SAFE_RELEASE(m_pVPEnum);
SAFE_RELEASE(m_pVP);
}
}
if (!m_pVP)
{
// 如果处理器不存在,就创建一个新的
// ... 创建 D3D11_VIDEO_PROCESSOR_CONTENT_DESC ...
hr = m_pVid->CreateVideoProcessor(m_pVPEnum, 0, &m_pVP);
// ...
}
这段代码确保了我们总是有个配置正确的视频处理器可供使用。
第二步:为输入和输出纹理创建视图
GPU 不能直接操作纹理本身,它需要通过"视图"来访问。
cpp
// 文件: Preproc.cpp (Convert)
// 为输入的 RGBA 纹理创建一个"输入视图"
D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC inputVD = { ... };
hr = m_pVid->CreateVideoProcessorInputView(pRGB, m_pVPEnum, &inputVD, &pVPIn);
// 为输出的 NV12 纹理创建一个"输出视图"
ID3D11VideoProcessorOutputView* pVPOV = nullptr;
// (这里有一个优化:代码会检查 viewMap 中是否已为该纹理创建过视图)
D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC ovD = { ... };
hr = m_pVid->CreateVideoProcessorOutputView(pYUV, m_pVPEnum, &ovD, &pVPOV);
第三步:执行转换
一切准备就绪,现在是时候让 GPU 开始工作了。
cpp
// 文件: Preproc.cpp (Convert)
// 准备一个"流"结构体,将输入视图放进去
D3D11_VIDEO_PROCESSOR_STREAM stream = { TRUE, 0, 0, 0, 0, nullptr, pVPIn, nullptr };
// 执行 Blt 操作!这是真正发生转换的地方
hr = m_pVidCtx->VideoProcessorBlt(m_pVP, pVPOV, 0, 1, &stream);
// ... 释放本次调用创建的临时资源 ...
SAFE_RELEASE(pVPIn);
当 VideoProcessorBlt
函数返回时,转换操作就已经在 GPU 上完成了。输出纹理 pYUV
(也就是 pEncBuf
)现在已经充满了新鲜出炉的 NV12 格式的图像数据。
总结
在本章中,我们深入了解了流水线中至关重要的"翻译官"------RGBToNV12
。
- 我们知道了它的核心任务是将捕获到的 RGBA 图像转换为编码器偏爱的 NV12 格式,这是为了实现更高效的视频压缩。
- 它的角色是流水线中的"色彩专家",利用 GPU 硬件加速能力完成这一转换,保证了整个流程的高性能。
- 我们学习了它的核心用法:通过
Convert()
方法,接收一个输入纹理和一个输出纹理,即可完成转换。 - 我们还探究了其内部原理,了解到它通过 DirectX 的视频处理器 (Video Processor) 和
VideoProcessorBlt
命令,将繁重的转换工作完全交给了 GPU 硬件。