云游戏技术之高速截屏和GPU硬编码 (5) 色彩空间转换器 (RGBToNV12)

在上一章中,我们认识了流水线终点NVENC 硬件编码器封装 (NvEncoderD3D11)。我们了解到,这位剪辑师虽然工作效率极高,但它有一个小小的"偏好":它最喜欢处理一种叫做 NV12 的特殊图像格式。

然而,我们流水线起点的桌面复制接口 (DDAImpl 捕获到的画面却是常见的 RGBA 格式。这就好比摄影师拍了一张色彩斑斓的数码照片(RGBA),而印刷厂的机器(编码器)只认一种特殊的印刷油墨模式(NV12)。

我们该如何解决这个"格式不兼容"的问题呢?

答案就是我们本章的主角,流水线中承上启下的关键一环:RGBToNV12------一位专业的"色彩空间转换器"。

RGBToNV12 是什么?

RGBToNV12 是我们流水线的第二步:预处理 (Pre-process)

它的任务非常专一:接收一张 RGBA 格式的图像,并利用显卡内置的视频处理功能,将其快速地转换为 NV12 格式。它就像一位专业的色彩专家,能将一幅色彩丰富的油画(RGB)转换成适合印刷的特定颜色模式(NV12),同时保证视觉效果基本不变。

为什么需要这种转换?

你可能会问,为什么不直接让编码器处理 RGBA 图像呢?

这背后是视频压缩的核心思想。

  • RGBA:存储了每个像素完整的红(R)、绿(G)、蓝(B)和透明度(A)信息。这种格式信息完整,但数据量巨大。
  • NV12 :是一种 YUV 颜色格式的变体。它将图像信息分为两部分:
    1. 亮度 (Luma, Y):代表图像的明暗信息,每个像素都有一个独立的亮度值。
    2. 色度 (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;
}

使用起来非常直观:

  1. 我们准备好输入 :一张从 DDAImpl 捕获的、包含 RGBA 图像的纹理 (pDupTex2D)。
  2. 我们准备好输出目标 :一张从 NvEncoderD3D11 "借"来的、空的 NV12 格式纹理 (pEncBuf)。
  3. 调用 pColorConv->Convert() 方法,它会施展"魔法",将 pDupTex2D 的内容转换后,填充到 pEncBuf 中。

转换完成后,pEncBuf 就包含了编码器所需要的 NV12 格式图像,可以被送入流水线的下一个环节了。

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

RGBToNV12 的高效并非源于复杂的 CPU 计算,而是巧妙地利用了 DirectX 中一个强大的功能:视频处理器 (Video Processor)。这是现代显卡普遍具备的硬件功能,专门用于视频播放中的各种图像处理,比如缩放、去隔行,以及我们这里用到的色彩空间转换。

转换的幕后故事

让我们用一个时序图来看看,当 Convert 方法被调用时,RGBToNV12 是如何与 DirectX 沟通,让 GPU 来完成实际工作的。

sequenceDiagram participant App as DemoApplication participant Converter as RGBToNV12 participant D3D aS DirectX 视频设备 participant GPU App->>Converter: Convert(pRGB, pYUV) Note right of Converter: 准备工作 Converter->>D3D: 为 pRGB 创建一个"输入视图" D3D-->>Converter: 输入视图已创建 Converter->>D3D: 为 pYUV 创建一个"输出视图" D3D-->>Converter: 输出视图已创建 Note right of Converter: 指挥 GPU 执行 Converter->>D3D: VideoProcessorBlt(输入视图 -> 输出视图) D3D->>GPU: 执行硬件色彩转换 GPU-->>D3D: 转换完成 D3D-->>Converter: Blt 操作成功 Converter-->>App: 转换成功

这个流程可以简化为三步:

  1. 创建"窗口"RGBToNV12 首先为输入的 RGBA 纹理和输出的 NV12 纹理分别创建了"输入视图"和"输出视图"。你可以把"视图(View)"想象成让视频处理器能够"看到"并操作这块显存区域的特殊窗口。
  2. 配置处理器 :它会确保内部的"视频处理器"(m_pVP)已经根据输入输出图像的尺寸等信息配置好了。
  3. 下达指令 :最后,它调用 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 硬件。

相关推荐
要做朋鱼燕3 小时前
【C++】Vector核心实现:类设计到迭代器陷阱
开发语言·c++·笔记·算法·职场和发展
学生小羊3 小时前
C++小游戏
开发语言·c++·游戏
企鹅chi月饼3 小时前
面试问题:c++的内存管理方式,delete的使用,vector的resize和reverse,容量拓展
c++·面试
hansang_IR4 小时前
【题解】洛谷P1776 宝物筛选 [单调队列优化多重背包]
c++·算法·动态规划·题解·背包·多重背包·单调队列
jndingxin4 小时前
c++多线程(1)------创建和管理线程td::thread
开发语言·c++·算法
SuperCandyXu4 小时前
洛谷 P3128 [USACO15DEC] Max Flow P -普及+/提高
c++·算法·图论·洛谷
拾光Ծ5 小时前
【STL】C++ 开发者必学字符类详解析:std::string
开发语言·c++
ajassi20006 小时前
开源 C++ QT Widget 开发(十二)图表--环境监测表盘
c++·qt·开源
zc.ovo6 小时前
牛子图论1(二分图+连通性)
数据结构·c++·算法·深度优先·图论