视频和视频帧:Intel GPU(核显)的编解码故事

视频和视频帧:Intel GPU(核显)的编解码故事

写在前面

一般称 基于“显卡或者多媒体处理芯片对视频进行解码”为硬解码 ,本文就将介绍如何用Intel的核显,即CPU上的集成GPU做硬解码。

QSV 全称 Quick Sync Video ,是Intel在2011年在发布其著名的CPU制程 Sandy Bridge 的时候一起发布的,这是一项基于其核显进行多媒体处理,包括视频编解码的技术。题外话插一句: 集成核显,官方称HD Graphics ,其实最早是在Sandy Bridge上一代制程的时候就推出的硬件技术,不过,核显整体的性能得到充分发挥和提升是在Sandy Bridge的时候;于是,在下一代制程Haswell及以后发布了比HD Graphics更高端的 Iris )。官方合称核显技术为 HD/Iris Graphic ,每代CPU的核显都在不断增强、内部结构也不断在调整,包括 执行单元 Execution Unit,EU )也在不断增加。前不久Intel对外宣称要做独立显卡了,之后其核显的发展具体如何就不得而知了。

在接手QSV项目的时候,原本以为QSV硬解应该有很多资料,实际上却相反,因此自己做个笔记记录下来。

本文将介绍:

  1. Intel核显的物理结构。包括 CPU核心、最后一级缓存LLC,GPU的Slice和Un-Slice结构,以及编解码的MFF模块
  2. QSV技术。主要包括Intel的QSV底层库结构。
  3. 基于 FFmpeg QSV解码插件开发 ,以及用 GPU做Memory-Mapping的优化

I. Intel的核显(集成GPU)

了解Intel的核显很有必要,说起来非常汗颜,笔者作为计算机专业出身,在几个月之前,笔者对CPU的认识还停留在上古的CPU“ 南北桥 ”架构上。所以,本章有任何不准确的地方,请指出,非常感谢!

首先看一张Gen11的CPU结构图(图片来自于 Intel Processor Graphics Gen11 Architecture ):

​图1. Gen11 CPU架构图

首先来看CPU核心部分:上图1右边整块灰色部分。

  • 中间灰色上半部分是 CPU Cores ,也就是常说的 CPU的物理核心:一块CPU上配置的计算单元个数,一般认为一块物理核心应该至少配备了L1(L2)缓存 。在Linux操作系统上,CPU的物理核心总数也可以在 /proc/cpuinfo 里面通过指令 cat /proc/cpuinfo 查看的到;除此之外,还有 CPU逻辑核心 的概念,这是 利用超线程技术模拟出来的核,一般来说,一个物理核可以虚拟出2个 。相信大部分读者还听说过“ 大小核 ”:小核负责日常任务,大核则负责大型应用。这样设计的目的主要是为了节省功耗。不过,不管大小核还是多核,都有可能因为底层设计缺陷而出现“ 一核有难多核围观 ”的问题,不过这个问题是芯片工程师们该操心的事儿,上层开发们,少写点逻辑代码BUG已经是谢天谢地了。(逃)
  • 中间灰色下半部分是 Shared LLC ,英文全称 Last Level Cache ,也就是我们常说的 最后一级缓存 。有些芯片上是第三级缓存,有些则是第四级缓存, 无论设计了多少级,最后一级缓存的作用都是一样的:物理核之间的数据交换 。再多说几句,CPU上的缓存作用不用多说: 弥补CPU和主存(也称内存,就是主板上的那几根内存条)的速度差太大,用于提高效率的 。芯片工程师在底层会设计一系列算法增加 缓存命中率 以减小I/O的开销,当然物理核和缓存之间的布线和工艺,也是非常重要的。
  • 最右边细长的灰条部分承担整块芯片类似Agent的部分,包括 显示控制部分、和外部做I/O部分的PCIe 等。这里不多做赘述。


现在看CPU的核显,集成GPU部分。

首先,从上图1中可以看到: Intel的核显在整块CPU芯片中的占比是不小的,而且现在核显的算力不容小觑 。想想在没有独立显卡的笔记本上,也是可以玩很多大型游戏,虽然偶有听人抱怨会卡、掉帧,但是整体的表现已经不错了。(逃,反正笔者不玩游戏)

接下来,看看官方给出的GPU内部的结构图:

  • Subslice:GPU的一个小型集成芯片 。图1中可以很明显看到,它包括8块EU芯片,还有承担Dataport、Sampler、Thread Dispatch的芯片。
    • EU:GPU的执行单元,负责所有通用的计算功能
    • Dataport:承担Subslice和GPU其他部分的数据交换职责 ,官方说还会做“Memory request coalescence(内存请求合并)”,从其他资料上查到是 聚合浮点数操作,即把浮点数放在一起处理,以提高整体带宽和处理效率
    • Sampler:有说法是做外部数据的纹理采样 。【但是这个是干什么用的,笔者完全没理解,这里记一笔】;
    • Thread Dispatch:用于给每个EU中的每个线程来分配任务 ,即,所有的指令先进入它内部的指令缓存,然后再由它来将这些指令分发给有空闲的EU。
​图2. Gen11 GPU架构放大图

GPU内部其实远比上图复杂,上文提到的也只是 GPU Slice部分的Subslice 芯片的结构, Slice 部分还有:

  • L3 Cache:负责Subslice之间的数据通信 ,作用和CPU核心上的 LLC 作用类似,只不过, LLC 负责的是CPU Core之间的数据通信。
  • Slice Common:官方说法是“能够支持2个甚至更多的subslice芯片工作的 ,可扩展的ff assets”,看上去似乎也是Subslice的辅助功能,官方解释的原文如下:【笔者没有深究,留个坑吧】
    Scalable fixed function assets which support the compute horsepower provided two or more subslices.


GPU可以分为Slice部分和Un-Slice部分。 上文已经介绍完整个 Slice 部分,接下来介绍 Un-Slice 部分,对应图2最上方部分。

  • GTI :全称 Graphics Technology Interface 负责GPU和CPU内存部分的数据通信(笔者认为,这里应该还包括指令数据) ,官方说法如下:
    the gateway between GPU and the rest of the SoC. The rest of the SoC includes memory hierarchy elements such as the shared LLC memory and system DRAM
    (GTI)是GPU与其他SoC之间的“网关”。 SoC的其余部分包括内存各层元素,例如LLC和系统DRAM
  • MFF Media Fixed Function ,这个模块一直没有找到明确的官方说法,在文档中还提到了 MFX/VQE/SFC ,笔者查阅了很多资料,整理如下:
    • MFX:全称Multi-format Codec,即多媒体解码器 ,在intel官方文档 Programmer's Reference Manual 中称为“Video Decode (VD) Box”,但是这个名称在外部搜索的时候资料远少于MFX,而且在很多流出来的PPT资料中看到的多是MFX模块。但是,无论名称如何, 这是负责多媒体编解码的独立模块,并且intel的QSV硬解,就是在这个模块上进行解码的
    • VQE:全称Visual Quality Enhancement,从名称来看是做视频增强的 真进步还是挤牙膏?Intel七代酷睿最全解析 的说法是:该模块能够以较低的能耗做色彩增强、矫正等。笔者的理解是, 除了编解码之外的大部分多媒体处理工作,都是该模块负责的
    • SFC:全称Scaler and Format Conversion ,这个模块的介绍资料就更少了,是负责视频scale和format conversion(格式转化,如NV12转NV21)原文是transcode(转码),这个地方描述不正确。

笔者在网上找到了一张图,见下图3。图中所示是在MFF上进行视频处理的整个流程:1). 首先在 MFX/VDBOX 模块上进行 编解码 ; 2). 接着送到 VQE/VEBOX 上做图像增强和矫正处理; 3). 然后送到 SFC 上做scale和transcode;4).最后送出到显示屏上展示。【至于真实是否如此,笔者在这里记一笔。】

​图3. MFF流程图

笔者推荐知乎文章 【转】Intel Gen8/Gen9核芯显卡微架构详细剖析 ,介绍的是第9代CPU结构的,文章深入浅出,上文关于thread dispatch的说明就是出自这篇文章。


最后,用网上找到的一张图(当时忘记记录来源了,侵删)总结Intel集成GPU/核显的结构:

​图4. GPU流程架构图
  • 中间部分写着“ EU ”的整块非常明显是 Subslice ,如上文介绍的,包括 Dataport和Sampler ,承担了整个GPU的绝大部分计算职责;
  • Subslice 上方紫色部分就是 MFF ,包括了 MFX\VQE\SFC ,承担了多媒体处理的职责。
  • Subslice 右边的蓝色等腰梯形是 Thread Dispatch ,分派具体的执行任务给具体 EU 芯片。
  • Thread Dispatch 右边蓝色矩形块应该就是 GTI 模块,从CPU中载入数据,最后把处理完毕的数据载出。

注意,这个是skylake架构的 GT2/GT3/GT4 的GPU结构图, GTX X 数字越大,集成的 Slice Unslice 芯片更多,能力越强,必然价格也更高。


II. Quick Sync Video(QSV)技术

QSV是Intel推出的将视频处理送到其GPU上专门负责视频处理的硬件模块处理的软件技术 ,维基百科上有一段话描述如下:

Unlike video encoding on a CPU or a general-purpose GPU, Quick Sync is a dedicated hardware core on the processor die. This allows for much more power efficient video processing
与CPU或通用GPU上的视频编码不同,Quick Sync是处理器芯片上的专用硬件核心。 这使得视频处理更加强大有效。

通过第一章的学习,笔者认为“ dedicated hardware core ”应该就是MFF模块组。要了解QSV如何驱动GPU的MFF,首先上一幅来自Intel官方 Intel® Video and Audio for Linux 上的图:

​图5. QSV-FFmpeg架构图

在介绍QSV之前,插一句题外话,Intel在FFmpeg上提供的插件除了 ffmpeg-qsv 之外,其他还有 ffmpeg-vaapi ffmpeg-ocl ,官方介绍如下(本文只介绍QSV,其他两类读者可以自行了解):

· FFmpeg-vaapi is an FFmpeg plugin, which supplies hardware acceleration based on the low-level VAAPI interface that takes advantage of the industry standard VA API to execute high-performance video codec, video processing, and transcoding capability on Intel GPU.
· FFmpeg-qsv is an FFmpeg plugin, which supplies hardware acceleration based on Intel GPU. It provides high-performance video codec, video processing, and transcoding capability based on Intel Media SDK library.
· FFmpeg-ocl is an FFmpeg plugin, which supplies hardware acceleration based on industrial standard OpenCL on CPU/GPU. It is mainly used to accelerate video processing filters.

· FFmpeg-vaapi是一个FFmpeg插件,它提供基于低级VAAPI接口的硬件加速,利用行业标准VAAPI在英特尔GPU上执行高性能视频编解码器,视频处理和转码功能。
· FFmpeg-qsv是一个FFmpeg插件,提供基于Intel GPU的硬件加速。 它提供基于Intel Media SDK库的高性能视频编解码器,视频处理和转码功能。
·FFmpeg-ocl是一个FFmpeg插件,它在CPU/GPU上提供基于工业标准OpenCL的硬件加速。 它主要用于加速视频处理过滤器。

言归正传,QSV插件在 ffmpeg2.8及以上 的版本就支持了,这个说法来自官方白皮书 Intel Quick Sync Video and FFmpeg Installation and Validation Guide 。上图5可以看到:QSV插件到底层需要经过 MSDK、LibVA、UMD和LibDRM ,那就一层一层来分析。

· MSDK

MSDK 的全称是 Media Software Development Kit ,是 Intel的媒体开发库 ,它支持多种图形平台(graphics platforms),实现了通用功能,能对数字视频进行预处理、编解码、以及不同编码格式的转换。该工具的源码地址在 Intel® Media SDK ,可以在Linux平台上编译使用。

· VA-API

VA-API 全称 Video Acceleration API 在用户态暴露可以操作核显的API,这是一套类unix平台提供视频硬件加速的开源库和标准,并不是Intel独有,NVIDIA和AMD都有对应的API工具 。Intel的源码地址在 Intel-vaapi-driver Project ,可以在Linux平台上使用,知乎的问题 FFmpeg为什么迟迟不启用vaapi解码/编码? 的高赞回答可以帮助快速了解VA-API。

· UMD

首先吐槽下Intel的官方的英文缩写实在太多了,而且好多缩写在Google上很难搜到相关的内容,UMD就是个例子。好在官网上解释了: UMD是User Mode Driver的缩写,在这里指的其实是VA-API Driver 。而Intel提供了2个工具: intel-vaapi-driver intel-media-driver ,前者是历史遗留的,后者是新提供的。推荐使用后者。

· LibDRM

DRM 全称是 Direct Rendering Manager ,即 直接渲染管理器 ,它是为了解决多个程序对 Video Card资源的协同使用问题而产生的。它向用户空间提供了一组 API,用以访问操纵 GPU。该段话引用自 Linux 下的 DRM ,从Intel官网中还可以知道,DRM还是一套跨多驱动管理的中间件,承接用户态的VA-API和内核态的各类driver。同VA-API,DRM也是一套Linux/Unix平台上通用的解决方案。

· Linux Kernel

Intel的kernel是i915 driver,历史上还有i810,笔者没有研究过这一块,只放一张libDRM和Kernel Driver之间的关系:

图6. libDRM和Kernel Driver关系图

由此,至上而下的调用层级关系已经结束,至于内核态的驱动如何将用户态的指令转化为底层驱动的指令,笔者肯定是不知道的(逃),但是到这里整个关系图应该比较清晰了。


III. FFMPEG+QSV解码

QSV硬解要做的事总结起来无外乎以下几件事:

  1. 在解码初始化时 打开底层驱动
  2. 解码之前 指定解码器codec为 h264_qsv
  3. 解码时把h264流数据 送到 GPU的解码模块上解码;
  4. 继续在GPU上执行后续操作 或者 送回 CPU/内存;

至于3-4步操作,比如怎么样把h264视频流送到MFF模块上、如何调用MFF模块完成解码工作,最后如何把数据送回CPU/内存,底层库会帮助完成。不过,想称为一个优秀的工程师,之后还是应该花时间认真研究下FFMPEG源码的。【记一笔,还需要继续深入学习】

接下来介绍如何在使用FFmpeg API中的 h264_qsv 解码器插件

插一句题外话,FFmpeg的命令行使用办法推荐阅读官方资料 QuickSync 或者 Intel_FFmpeg_plugins ,前者还介绍了每一代Intel CPU产品增加了哪些codec(H264解码器很早就支持,H265直到skylake才完全支持):

图7. 每代CPU的硬件支持表

关于sample code,笔者踩过很多坑,总结一句话: 大多数中文博客太不可信,还是要相信官方demo 。然而官方的demo挺多的,笔者亲身实践来说,肯定有2份官方代码可用: qsvdec.c hw_decode.c 。笔者最早用的时第一段代码,硬解相关的核心部分如下:

    /* open the hardware device - 打开硬件!!!必须放在前面完成 */
    ret = av_hwdevice_ctx_create(&decode.hw_device_ref, AV_HWDEVICE_TYPE_QSV,
                                 "auto", NULL, 0);
    if (ret < 0) {
        fprintf(stderr, "Cannot open the hardware device\n");
        goto finish;
    /* initialize the decoder - 注册使用h264_qsv codec */
    decoder = avcodec_find_decoder_by_name("h264_qsv");
    if (!decoder) {
        fprintf(stderr, "The QSV decoder is not present in libavcodec\n");
        goto finish;
    // 常规操作 - 复制codec
    decoder_ctx = avcodec_alloc_context3(decoder);
    if (!decoder_ctx) {
        ret = AVERROR(ENOMEM);
        goto finish;
    // 硬解相关代码
    decoder_ctx->codec_id = AV_CODEC_ID_H264;
    if (video_st->codecpar->extradata_size) {
        decoder_ctx->extradata = av_mallocz(video_st->codecpar->extradata_size +
                                            AV_INPUT_BUFFER_PADDING_SIZE);
        if (!decoder_ctx->extradata) {
            ret = AVERROR(ENOMEM);
            goto finish;
        memcpy(decoder_ctx->extradata, video_st->codecpar->extradata,
               video_st->codecpar->extradata_size);